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
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ public static string MockClass(
else
{
sb.Append("\t\t\tmockBehavior ??= global::Mockolate.MockBehavior.Default;").AppendLine();
sb.Append("\t\t\tglobal::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.").Append(name).Append(".CreateFastInteractions(mockBehavior), constructorParameters);").AppendLine();
sb.Append("\t\t\tglobal::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.").Append(name).Append(".MemberCount, constructorParameters);").AppendLine();
}

sb.Append("\t\t\treturn CreateMockInstance(mockRegistry, constructorParameters, setup);").AppendLine();
Expand Down Expand Up @@ -1383,7 +1383,7 @@ private static void AppendCreateRegistryFromBehavior(StringBuilder sb, string in
sb.Append(indent).Append("/// </summary>").AppendLine();
sb.Append(indent).Append("private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior)").AppendLine();
sb.Append(indent).Append("{").AppendLine();
sb.Append(indent).Append("\tglobal::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior));").AppendLine();
sb.Append(indent).Append("\tglobal::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount);").AppendLine();
if (setsMockRegistryProvider)
{
sb.Append(indent).Append("\tMockRegistryProvider.Value = registry;").AppendLine();
Expand Down
21 changes: 17 additions & 4 deletions Source/Mockolate/Interactions/FastMockInteractions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,20 @@ public class FastMockInteractions : IMockInteractions
private long _globalSequence;

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly MockolateLock _verifiedLock = new();
[field: DebuggerBrowsable(DebuggerBrowsableState.Never)]
private MockolateLock VerifiedLock
{
get
{
if (field is { } existing)
{
return existing;
}

Interlocked.CompareExchange(ref field, new MockolateLock(), null);
return field!;
}
}

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private HashSet<IInteraction>? _verified;
Expand Down Expand Up @@ -191,7 +204,7 @@ public IReadOnlyCollection<IInteraction> GetUnverifiedInteractions()
unverified.Sort(static (left, right) => left.Seq.CompareTo(right.Seq));
}

lock (_verifiedLock)
lock (VerifiedLock)
{
if (_verified is null || _verified.Count == 0)
{
Expand Down Expand Up @@ -224,7 +237,7 @@ public IReadOnlyCollection<IInteraction> GetUnverifiedInteractions()

void IMockInteractions.Verified(IEnumerable<IInteraction> interactions)
{
lock (_verifiedLock)
lock (VerifiedLock)
{
_verified ??= [];
foreach (IInteraction interaction in interactions)
Expand All @@ -245,7 +258,7 @@ public void Clear()

Volatile.Read(ref _fallback)?.Clear();

lock (_verifiedLock)
lock (VerifiedLock)
{
_verified = null;
}
Expand Down
30 changes: 22 additions & 8 deletions Source/Mockolate/MockRegistry.Interactions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@ namespace Mockolate;

public partial class MockRegistry
{
private IMockInteractions? _interactions;
private readonly int _interactionMemberCount;

/// <summary>
/// Gets the collection of interactions recorded by the mock object.
/// </summary>
public IMockInteractions Interactions { get; }
public IMockInteractions Interactions => _interactions ?? EnsureInteractions();

private IMockInteractions EnsureInteractions()
{
Interlocked.CompareExchange(ref _interactions,
new FastMockInteractions(_interactionMemberCount, Behavior.SkipInteractionRecording), null);
return _interactions!;
}

/// <summary>
/// Clears all interactions recorded by the mock object.
Expand Down Expand Up @@ -131,16 +141,17 @@ public void ClearAllInteractions()
/// <returns>A lazy stream of matching setups, scenario-scoped first.</returns>
public IEnumerable<T> GetMethodSetups<T>(string methodName) where T : MethodSetup
{
if (!string.IsNullOrEmpty(Scenario) &&
Setup.TryGetScenario(Scenario, out MockScenarioSetup? scopedBucket))
MockSetups? setups = _setups;
if (!string.IsNullOrEmpty(Scenario) && setups is not null &&
setups.TryGetScenario(Scenario, out MockScenarioSetup? scopedBucket))
{
return EnumerateScopedAndGlobalMethodSetups<T>(methodName, scopedBucket);
}

MethodSetup[]?[]? snapshot = Volatile.Read(ref _setupsByMemberId);
if (snapshot is null)
{
return Setup.Methods.EnumerateByName<T>(methodName);
return setups is null ? Array.Empty<T>() : setups.Methods.EnumerateByName<T>(methodName);
}

return EnumerateGlobalMethodSetups<T>(methodName, snapshot);
Expand Down Expand Up @@ -178,11 +189,14 @@ private IEnumerable<T> EnumerateGlobalMethodSetups<T>(string methodName,
}

// Hand-written SetupMethod(MethodSetup) entries (e.g. the HttpClientExtensions pipeline) live
// only in the root dict; the empty-storage fast path returns Array.Empty<T> so the loop
// allocates nothing further when no such entry exists.
foreach (T setup in Setup.Methods.EnumerateByName<T>(methodName))
// only in the root dict; when no MockSetups has been allocated there can be none, so the loop
// is skipped entirely and never forces the lazy allocation.
if (_setups is { } setups)
{
yield return setup;
foreach (T setup in setups.Methods.EnumerateByName<T>(methodName))
{
yield return setup;
}
}
}

Expand Down
36 changes: 29 additions & 7 deletions Source/Mockolate/MockRegistry.Setup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,28 @@ namespace Mockolate;
public partial class MockRegistry
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly object _setupsByMemberIdLock = new();
[field: DebuggerBrowsable(DebuggerBrowsableState.Never)]
private object SetupsByMemberIdLock
{
get
{
if (field is { } existing)
{
return existing;
}

Interlocked.CompareExchange(ref field, new object(), null);
return field!;
}
}

/// <summary>
/// Returns the generator-known member count hint when <see cref="Interactions" /> is a
/// <see cref="FastMockInteractions" />, so dispatch tables can be sized once on first allocation
/// instead of growing one slot at a time as setups for higher-numbered members come in.
/// </summary>
private int GetMemberCountHint()
=> Interactions is FastMockInteractions fast ? fast.Buffers.Length : 0;
=> _interactions is FastMockInteractions fast ? fast.Buffers.Length : _interactionMemberCount;

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private EventSetup[]?[]? _eventSetupsByMemberId;
Expand All @@ -31,10 +44,19 @@ private int GetMemberCountHint()
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private MethodSetup[]?[]? _setupsByMemberId;

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private MockSetups? _setups;

/// <summary>
/// The registered setups for the mock, including methods, properties, indexers and events.
/// </summary>
internal MockSetups Setup { get; }
internal MockSetups Setup => _setups ?? EnsureSetups();

private MockSetups EnsureSetups()
{
Interlocked.CompareExchange(ref _setups, new MockSetups(), null);
return _setups!;
}

/// <summary>
/// Registers <paramref name="indexerSetup" /> for the default scenario.
Expand Down Expand Up @@ -88,7 +110,7 @@ public void SetupIndexer(int memberId, string scenario, IndexerSetup indexerSetu

private void AppendToIndexerMemberIdBucket(int memberId, IndexerSetup indexerSetup)
{
lock (_setupsByMemberIdLock)
lock (SetupsByMemberIdLock)
{
IndexerSetup[]?[]? oldTable = _indexerSetupsByMemberId;
int requiredLen = memberId + 1;
Expand Down Expand Up @@ -184,7 +206,7 @@ public void SetupMethod(int memberId, string scenario, MethodSetup methodSetup)

private void AppendToMemberIdBucket(int memberId, MethodSetup methodSetup)
{
lock (_setupsByMemberIdLock)
lock (SetupsByMemberIdLock)
{
MethodSetup[]?[]? oldTable = _setupsByMemberId;
int requiredLen = memberId + 1;
Expand Down Expand Up @@ -279,7 +301,7 @@ public void SetupProperty(int memberId, string scenario, PropertySetup propertyS

private void PublishPropertyToMemberIdBucket(int memberId, PropertySetup propertySetup)
{
lock (_setupsByMemberIdLock)
lock (SetupsByMemberIdLock)
{
PropertySetup?[]? oldTable = _propertySetupsByMemberId;
int requiredLen = memberId + 1;
Expand Down Expand Up @@ -369,7 +391,7 @@ public void SetupEvent(int memberId, string scenario, EventSetup eventSetup)

private void AppendToEventMemberIdBucket(int memberId, EventSetup eventSetup)
{
lock (_setupsByMemberIdLock)
lock (SetupsByMemberIdLock)
{
EventSetup[]?[]? oldTable = _eventSetupsByMemberId;
int requiredLen = memberId + 1;
Expand Down
62 changes: 47 additions & 15 deletions Source/Mockolate/MockRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Threading;
using Mockolate.Interactions;
using Mockolate.Setup;

Expand All @@ -18,7 +19,7 @@ namespace Mockolate;
public partial class MockRegistry
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly ScenarioState _scenarioState;
private ScenarioState? _scenarioState;

/// <summary>
/// Creates a new <see cref="MockRegistry" /> with the given <paramref name="behavior" />, a caller-provided
Expand Down Expand Up @@ -46,9 +47,29 @@ public MockRegistry(MockBehavior behavior, IMockInteractions interactions,

Behavior = behavior;
ConstructorParameters = constructorParameters;
Interactions = interactions;
Setup = new MockSetups();
_scenarioState = new ScenarioState();
_interactions = interactions;
Wraps = null;
}

/// <summary>
/// Creates a new <see cref="MockRegistry" /> with the given <paramref name="behavior" /> whose interaction
/// store is a <see cref="FastMockInteractions" /> sized to <paramref name="memberCount" />, allocated lazily
/// on first access.
/// </summary>
/// <remarks>
/// The generator-emitted <c>CreateMock</c> paths use this overload so that a mock which is only created — and
/// never has a member invoked or verified — never allocates an interaction store at all.
/// </remarks>
/// <param name="behavior">The <see cref="MockBehavior" /> that governs how the mock responds without a matching setup.</param>
/// <param name="memberCount">The number of distinct mockable members the lazily-created interaction store should hold.</param>
/// <param name="constructorParameters">
/// Values forwarded to the base-class constructor, or <see langword="null" /> if no base constructor call is needed.
/// </param>
public MockRegistry(MockBehavior behavior, int memberCount, object?[]? constructorParameters = null)
{
Behavior = behavior;
ConstructorParameters = constructorParameters;
_interactionMemberCount = memberCount;
Wraps = null;
}

Expand All @@ -62,9 +83,9 @@ public MockRegistry(MockRegistry registry, object wraps)
{
Behavior = registry.Behavior;
ConstructorParameters = registry.ConstructorParameters;
Interactions = new FastMockInteractions(0, registry.Behavior.SkipInteractionRecording);
Setup = registry.Setup;
_scenarioState = registry._scenarioState;
_interactions = new FastMockInteractions(0, registry.Behavior.SkipInteractionRecording);
_setups = registry.Setup;
_scenarioState = registry.GetOrCreateScenarioState();
Wraps = wraps;
}

Expand All @@ -78,9 +99,9 @@ public MockRegistry(MockRegistry registry, object?[] constructorParameters)
{
Behavior = registry.Behavior;
ConstructorParameters = constructorParameters;
Interactions = registry.Interactions;
Setup = registry.Setup;
_scenarioState = registry._scenarioState;
_interactions = registry.Interactions;
_setups = registry.Setup;
_scenarioState = registry.GetOrCreateScenarioState();
Wraps = registry.Wraps;
}

Expand All @@ -104,9 +125,9 @@ public MockRegistry(MockRegistry registry, IMockInteractions interactions)

Behavior = registry.Behavior;
ConstructorParameters = registry.ConstructorParameters;
Interactions = interactions;
Setup = registry.Setup;
_scenarioState = registry._scenarioState;
_interactions = interactions;
_setups = registry.Setup;
_scenarioState = registry.GetOrCreateScenarioState();
Wraps = registry.Wraps;
}

Expand All @@ -131,7 +152,7 @@ public MockRegistry(MockRegistry registry, IMockInteractions interactions)
/// Scenario setups add to, rather than replace, the default bucket - register catch-alls at the default scope
/// and override specific members per scenario.
/// </remarks>
public string Scenario => _scenarioState.Current;
public string Scenario => _scenarioState?.Current ?? "";

/// <summary>
/// Gets the behavior settings used by this mock instance.
Expand Down Expand Up @@ -166,7 +187,18 @@ public MockRegistry(MockRegistry registry, IMockInteractions interactions)
/// for the full resolution order.
/// </remarks>
public void TransitionTo(string scenario)
=> _scenarioState.Current = scenario;
=> GetOrCreateScenarioState().Current = scenario;

private ScenarioState GetOrCreateScenarioState()
{
if (_scenarioState is { } existing)
{
return existing;
}

Interlocked.CompareExchange(ref _scenarioState, new ScenarioState(), null);
return _scenarioState!;
}

private sealed class ScenarioState
{
Expand Down
1 change: 1 addition & 0 deletions Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ namespace Mockolate
public MockRegistry(Mockolate.MockRegistry registry, object wraps) { }
public MockRegistry(Mockolate.MockRegistry registry, object?[] constructorParameters) { }
public MockRegistry(Mockolate.MockBehavior behavior, Mockolate.Interactions.IMockInteractions interactions, object?[]? constructorParameters = null) { }
public MockRegistry(Mockolate.MockBehavior behavior, int memberCount, object?[]? constructorParameters = null) { }
public Mockolate.MockBehavior Behavior { get; }
public object?[]? ConstructorParameters { get; }
public Mockolate.Interactions.IMockInteractions Interactions { get; }
Expand Down
1 change: 1 addition & 0 deletions Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ namespace Mockolate
public MockRegistry(Mockolate.MockRegistry registry, object wraps) { }
public MockRegistry(Mockolate.MockRegistry registry, object?[] constructorParameters) { }
public MockRegistry(Mockolate.MockBehavior behavior, Mockolate.Interactions.IMockInteractions interactions, object?[]? constructorParameters = null) { }
public MockRegistry(Mockolate.MockBehavior behavior, int memberCount, object?[]? constructorParameters = null) { }
public Mockolate.MockBehavior Behavior { get; }
public object?[]? ConstructorParameters { get; }
public Mockolate.Interactions.IMockInteractions Interactions { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ namespace Mockolate
public MockRegistry(Mockolate.MockRegistry registry, object wraps) { }
public MockRegistry(Mockolate.MockRegistry registry, object?[] constructorParameters) { }
public MockRegistry(Mockolate.MockBehavior behavior, Mockolate.Interactions.IMockInteractions interactions, object?[]? constructorParameters = null) { }
public MockRegistry(Mockolate.MockBehavior behavior, int memberCount, object?[]? constructorParameters = null) { }
public Mockolate.MockBehavior Behavior { get; }
public object?[]? ConstructorParameters { get; }
public Mockolate.Interactions.IMockInteractions Interactions { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal class ComprehensiveAbstractClass :
/// </summary>
private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior)
{
global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior));
global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount);
MockRegistryProvider.Value = registry;
return registry;
}
Expand Down Expand Up @@ -943,7 +943,7 @@ internal static partial class MockExtensionsForComprehensiveAbstractClass
}

mockBehavior ??= global::Mockolate.MockBehavior.Default;
global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ComprehensiveAbstractClass.CreateFastInteractions(mockBehavior), constructorParameters);
global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ComprehensiveAbstractClass.MemberCount, constructorParameters);
return CreateMockInstance(mockRegistry, constructorParameters, setup);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal class ICombinationMockA :
/// </summary>
private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior)
{
global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior));
global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount);
return registry;
}

Expand Down Expand Up @@ -544,7 +544,7 @@ internal static partial class MockExtensionsForICombinationMockA
}

mockBehavior ??= global::Mockolate.MockBehavior.Default;
global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ICombinationMockA.CreateFastInteractions(mockBehavior), constructorParameters);
global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ICombinationMockA.MemberCount, constructorParameters);
return CreateMockInstance(mockRegistry, constructorParameters, setup);
}

Expand Down
Loading
Loading