From 664b82f9f5898453964697794fbc8bbcad9355b3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:33:29 +0100 Subject: [PATCH 1/9] perf: optimize TUnit.Mocks hot paths for mock creation, setup, and invocation - Replace ConditionalWeakTable with IMockObject interface for O(1) mock-to-wrapper lookup - Replace Lazy with nullable fields and manual init pattern (saves 48+ bytes per setup) - Defer allocation of MockEngine collections until first use (lazy-init pattern) - Remove factory delegate wrapper closures via delegate covariance - Keep RecordCall lock-free (ConcurrentQueue.Enqueue only) - Use direct lock reference instead of EnsureBehaviorLock() in read paths --- ...nheriting_Multiple_Interfaces.verified.txt | 24 +++--- .../Interface_With_Async_Methods.verified.txt | 58 +++++++------- .../Interface_With_Events.verified.txt | 24 +++--- ...nterface_With_Generic_Methods.verified.txt | 5 +- ...rited_Static_Abstract_Members.verified.txt | 5 +- ..._With_Keyword_Parameter_Names.verified.txt | 41 +++++----- .../Interface_With_Mixed_Members.verified.txt | 41 +++++----- ...ble_Reference_Type_Parameters.verified.txt | 77 +++++++++---------- ...rface_With_Out_Ref_Parameters.verified.txt | 41 +++++----- ...rface_With_Overloaded_Methods.verified.txt | 73 +++++++++--------- .../Interface_With_Properties.verified.txt | 5 +- ...ace_With_RefStruct_Parameters.verified.txt | 5 +- ..._With_Static_Abstract_Members.verified.txt | 5 +- ...stract_Transitive_Return_Type.verified.txt | 22 +++--- .../Multi_Method_Interface.verified.txt | 39 +++++----- ...ple_Interface_With_One_Method.verified.txt | 22 +++--- .../Builders/MockImplBuilder.cs | 19 ++++- .../Builders/MockMembersBuilder.cs | 40 +++++----- TUnit.Mocks/IMockObject.cs | 19 +++++ TUnit.Mocks/Mock.cs | 34 ++------ TUnit.Mocks/MockEngine.cs | 53 +++++++++---- TUnit.Mocks/MockMethodCall.cs | 16 ++-- TUnit.Mocks/MockOfT.cs | 5 +- TUnit.Mocks/Setup/MethodSetup.cs | 47 +++++++---- TUnit.Mocks/VoidMockMethodCall.cs | 18 ++--- 25 files changed, 409 insertions(+), 329 deletions(-) create mode 100644 TUnit.Mocks/IMockObject.cs diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt index aae5a4002c..855ee3ceb8 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IReadWriter_MockImpl : global::IReadWriter, global::TUnit.Mocks.IRaisable + internal sealed class IReadWriter_MockImpl : global::IReadWriter, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IReadWriter_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -105,7 +108,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy _lazyBuilder; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; internal IReadWriter_Write_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -113,17 +116,16 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } - ); - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } /// public IReadWriter_Write_M2_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt index 8df17a1351..68045385da 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IAsyncService_MockImpl : global::IAsyncService, global::TUnit.Mocks.IRaisable + internal sealed class IAsyncService_MockImpl : global::IAsyncService, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IAsyncService_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -162,7 +165,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IAsyncService_GetValueAsync_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -170,16 +173,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IAsyncService_GetValueAsync_M0_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -251,7 +253,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IAsyncService_ComputeAsync_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -259,16 +261,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IAsyncService_ComputeAsync_M2_MockCall Returns(int value) { EnsureSetup().Returns(value); return this; } @@ -340,7 +341,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy _lazyBuilder; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; internal IAsyncService_InitializeAsync_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -348,17 +349,16 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } - ); - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } /// public IAsyncService_InitializeAsync_M3_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt index 7040397997..6e8e905cea 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt @@ -61,10 +61,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class INotifier_MockImpl : global::INotifier, global::TUnit.Mocks.IRaisable + internal sealed class INotifier_MockImpl : global::INotifier, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal INotifier_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -140,7 +143,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy _lazyBuilder; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; internal INotifier_Notify_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -148,17 +151,16 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } - ); - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } /// public INotifier_Notify_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt index 68f87ae580..34307f8987 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IRepository_MockImpl : global::IRepository, global::TUnit.Mocks.IRaisable + internal sealed class IRepository_MockImpl : global::IRepository, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IRepository_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Inherited_Static_Abstract_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Inherited_Static_Abstract_Members.verified.txt index dc8c623623..60df228ce1 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Inherited_Static_Abstract_Members.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Inherited_Static_Abstract_Members.verified.txt @@ -63,10 +63,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IMyService_MockImpl : global::TUnit.Mocks.Generated.IMyService_Mockable, global::TUnit.Mocks.IRaisable + internal sealed class IMyService_MockImpl : global::TUnit.Mocks.Generated.IMyService_Mockable, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IMyService_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt index ccc0455c03..f7760f1478 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class ITest_MockImpl : global::ITest, global::TUnit.Mocks.IRaisable + internal sealed class ITest_MockImpl : global::ITest, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal ITest_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -116,7 +119,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy _lazyBuilder; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; internal ITest_Test_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -124,17 +127,16 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } - ); - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } /// public ITest_Test_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } @@ -191,7 +193,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal ITest_Get_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -199,16 +201,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public ITest_Get_M1_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt index 4b83f966ab..ac5874f1c8 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt @@ -61,10 +61,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IService_MockImpl : global::IService, global::TUnit.Mocks.IRaisable + internal sealed class IService_MockImpl : global::IService, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IService_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -186,7 +189,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IService_GetAsync_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -194,16 +197,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IService_GetAsync_M3_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -278,7 +280,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy _lazyBuilder; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; internal IService_Process_M4_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -286,17 +288,16 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } - ); - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } /// public IService_Process_M4_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt index 94d54e07ad..3437653b24 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IFoo_MockImpl : global::IFoo, global::TUnit.Mocks.IRaisable + internal sealed class IFoo_MockImpl : global::IFoo, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IFoo_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -219,7 +222,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy _lazyBuilder; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; internal IFoo_Bar_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -227,17 +230,16 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } - ); - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } /// public IFoo_Bar_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } @@ -294,7 +296,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IFoo_GetValue_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -302,16 +304,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IFoo_GetValue_M1_MockCall Returns(string? value) { EnsureSetup().Returns(value); return this; } @@ -383,7 +384,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy _lazyBuilder; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; internal IFoo_Process_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -391,17 +392,16 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } - ); - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } /// public IFoo_Process_M2_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } @@ -458,7 +458,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IFoo_GetAsync_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -466,16 +466,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IFoo_GetAsync_M3_MockCall Returns(string? value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt index fdb45bb5ea..e2266e56d2 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IDictionary_MockImpl : global::IDictionary, global::TUnit.Mocks.IRaisable + internal sealed class IDictionary_MockImpl : global::IDictionary, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IDictionary_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -107,7 +110,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IDictionary_TryGetValue_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -115,16 +118,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IDictionary_TryGetValue_M0_MockCall Returns(bool value) { EnsureSetup().Returns(value); return this; } @@ -200,7 +202,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy _lazyBuilder; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; internal IDictionary_Swap_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -208,17 +210,16 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } - ); - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } /// public IDictionary_Swap_M1_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt index cc74b2811e..7d0139abda 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IFormatter_MockImpl : global::IFormatter, global::TUnit.Mocks.IRaisable + internal sealed class IFormatter_MockImpl : global::IFormatter, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IFormatter_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -199,7 +202,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IFormatter_Format_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -207,16 +210,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IFormatter_Format_M0_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -288,7 +290,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IFormatter_Format_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -296,16 +298,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IFormatter_Format_M1_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -377,7 +378,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IFormatter_Format_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -385,16 +386,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IFormatter_Format_M2_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -466,7 +466,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IFormatter_Format_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -474,16 +474,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IFormatter_Format_M3_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Properties.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Properties.verified.txt index 75d2bee750..1408e0e6d0 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Properties.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Properties.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IRepository_MockImpl : global::IRepository, global::TUnit.Mocks.IRaisable + internal sealed class IRepository_MockImpl : global::IRepository, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IRepository_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_RefStruct_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_RefStruct_Parameters.verified.txt index 4603818ba5..47ff789005 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_RefStruct_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_RefStruct_Parameters.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IBufferProcessor_MockImpl : global::IBufferProcessor, global::TUnit.Mocks.IRaisable + internal sealed class IBufferProcessor_MockImpl : global::IBufferProcessor, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IBufferProcessor_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Members.verified.txt index 9a09752e23..4f298aeab3 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Members.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Members.verified.txt @@ -79,10 +79,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IServiceFactory_MockImpl : global::TUnit.Mocks.Generated.IServiceFactory_Mockable, global::TUnit.Mocks.IRaisable + internal sealed class IServiceFactory_MockImpl : global::TUnit.Mocks.Generated.IServiceFactory_Mockable, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IServiceFactory_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt index bee541a03d..2b37248eed 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IMyService_MockImpl : global::IMyService, global::TUnit.Mocks.IRaisable + internal sealed class IMyService_MockImpl : global::IMyService, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IMyService_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -100,7 +103,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IMyService_GetValue_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -108,16 +111,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IMyService_GetValue_M0_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt index 1b4c77e37f..2e5c7cbe46 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class ICalculator_MockImpl : global::ICalculator, global::TUnit.Mocks.IRaisable + internal sealed class ICalculator_MockImpl : global::ICalculator, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal ICalculator_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -142,7 +145,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal ICalculator_Add_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -150,16 +153,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public ICalculator_Add_M0_MockCall Returns(int value) { EnsureSetup().Returns(value); return this; } @@ -231,7 +233,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal ICalculator_Subtract_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -239,16 +241,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public ICalculator_Subtract_M1_MockCall Returns(int value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt index 4ed109f3f2..edadb4f78a 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt @@ -30,10 +30,13 @@ namespace TUnit.Mocks.Generated namespace TUnit.Mocks.Generated { - internal sealed class IGreeter_MockImpl : global::IGreeter, global::TUnit.Mocks.IRaisable + internal sealed class IGreeter_MockImpl : global::IGreeter, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject { private readonly global::TUnit.Mocks.MockEngine _engine; + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + internal IGreeter_MockImpl(global::TUnit.Mocks.MockEngine engine) { _engine = engine; @@ -83,7 +86,7 @@ namespace TUnit.Mocks.Generated private readonly int _memberId; private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; - private readonly global::System.Lazy> _lazyBuilder; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; internal IGreeter_Greet_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -91,16 +94,15 @@ namespace TUnit.Mocks.Generated _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new global::System.Lazy>(() => - { - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } - ); } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } /// public IGreeter_Greet_M0_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 518deccb33..99c83d7e4b 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -45,11 +45,13 @@ private static void BuildInterfaceMockImpl(CodeWriter writer, MockTypeModel mode baseTypes += ", " + string.Join(", ", model.AdditionalInterfaceNames); } - using (writer.Block($"internal sealed class {safeName}_MockImpl : {baseTypes}, global::TUnit.Mocks.IRaisable")) + using (writer.Block($"internal sealed class {safeName}_MockImpl : {baseTypes}, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject")) { writer.AppendLine($"private readonly global::TUnit.Mocks.MockEngine<{mockableType}> _engine;"); writer.AppendLine(); + EmitIMockObjectProperty(writer); + // Constructor using (writer.Block($"internal {safeName}_MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine)")) { @@ -95,12 +97,14 @@ private static void BuildWrapMockImpl(CodeWriter writer, MockTypeModel model, st { var mockableType = GetMockableTypeName(model); - using (writer.Block($"internal sealed class {safeName}_WrapMockImpl : {model.FullyQualifiedName}, global::TUnit.Mocks.IRaisable")) + using (writer.Block($"internal sealed class {safeName}_WrapMockImpl : {model.FullyQualifiedName}, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject")) { writer.AppendLine($"private readonly global::TUnit.Mocks.MockEngine<{mockableType}> _engine;"); writer.AppendLine($"private readonly {model.FullyQualifiedName} _wrappedInstance;"); writer.AppendLine(); + EmitIMockObjectProperty(writer); + // Generate constructors that pass through to base + accept wrapped instance GenerateWrapConstructors(writer, model, safeName); @@ -421,11 +425,13 @@ private static void BuildPartialMockImpl(CodeWriter writer, MockTypeModel model, { var mockableType = GetMockableTypeName(model); - using (writer.Block($"internal sealed class {safeName}_MockImpl : {model.FullyQualifiedName}, global::TUnit.Mocks.IRaisable")) + using (writer.Block($"internal sealed class {safeName}_MockImpl : {model.FullyQualifiedName}, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject")) { writer.AppendLine($"private readonly global::TUnit.Mocks.MockEngine<{mockableType}> _engine;"); writer.AppendLine(); + EmitIMockObjectProperty(writer); + // Generate constructors that pass through to base GeneratePartialConstructors(writer, model, safeName); @@ -1268,6 +1274,13 @@ public static string GetMockableTypeName(MockTypeModel model) /// Emits the static engine assignment with a guard that detects multiple mocks of the same /// static-abstract interface type within a single test context. /// + private static void EmitIMockObjectProperty(CodeWriter writer) + { + writer.AppendLine("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + writer.AppendLine("global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; }"); + writer.AppendLine(); + } + internal static void EmitStaticEngineAssignment(CodeWriter writer, string safeName) { writer.AppendLine($"if ({safeName}_StaticEngine.Engine is not null)"); diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 2de65e25f4..6f5e9e0327 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -153,10 +153,10 @@ private static void GenerateReturnUnifiedClass(CodeWriter writer, string wrapper writer.AppendLine("private readonly int _memberId;"); writer.AppendLine("private readonly string _memberName;"); writer.AppendLine("private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers;"); - writer.AppendLine($"private readonly global::System.Lazy<{builderType}> _lazyBuilder;"); + writer.AppendLine($"private {builderType}? _builder;"); writer.AppendLine(); - // Constructor — lazy registration via Lazy for thread safety + // Constructor writer.AppendLine($"internal {wrapperName}(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers)"); using (writer.Block()) { @@ -164,19 +164,18 @@ private static void GenerateReturnUnifiedClass(CodeWriter writer, string wrapper writer.AppendLine("_memberId = memberId;"); writer.AppendLine("_memberName = memberName;"); writer.AppendLine("_matchers = matchers;"); - using (writer.Block($"_lazyBuilder = new global::System.Lazy<{builderType}>(() =>")) - { - writer.AppendLine("var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); - writer.AppendLine("_engine.AddSetup(setup);"); - writer.AppendLine($"return new {builderType}(setup);"); - } - writer.AppendLine(");"); } writer.AppendLine(); // EnsureSetup method - writer.AppendLine($"private {builderType} EnsureSetup() => _lazyBuilder.Value;"); + using (writer.Block($"private {builderType} EnsureSetup()")) + { + writer.AppendLine("if (_builder is { } b) return b;"); + writer.AppendLine("var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); + writer.AppendLine("_engine.AddSetup(setup);"); + writer.AppendLine($"return _builder = new {builderType}(setup);"); + } writer.AppendLine(); @@ -290,10 +289,10 @@ private static void GenerateVoidUnifiedClass(CodeWriter writer, string wrapperNa writer.AppendLine("private readonly int _memberId;"); writer.AppendLine("private readonly string _memberName;"); writer.AppendLine("private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers;"); - writer.AppendLine($"private readonly global::System.Lazy<{builderType}> _lazyBuilder;"); + writer.AppendLine($"private {builderType}? _builder;"); writer.AppendLine(); - // Constructor — Lazy for thread safety, eagerly materialized because void methods + // Constructor — eagerly registers because void methods // are commonly used without chaining (e.g., mock.Log(Arg.Any()) in strict mode). writer.AppendLine($"internal {wrapperName}(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers)"); using (writer.Block()) @@ -302,20 +301,19 @@ private static void GenerateVoidUnifiedClass(CodeWriter writer, string wrapperNa writer.AppendLine("_memberId = memberId;"); writer.AppendLine("_memberName = memberName;"); writer.AppendLine("_matchers = matchers;"); - using (writer.Block($"_lazyBuilder = new global::System.Lazy<{builderType}>(() =>")) - { - writer.AppendLine("var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); - writer.AppendLine("_engine.AddSetup(setup);"); - writer.AppendLine($"return new {builderType}(setup);"); - } - writer.AppendLine(");"); - writer.AppendLine("_ = _lazyBuilder.Value;"); + writer.AppendLine("_ = EnsureSetup();"); } writer.AppendLine(); // EnsureSetup method - writer.AppendLine($"private {builderType} EnsureSetup() => _lazyBuilder.Value;"); + using (writer.Block($"private {builderType} EnsureSetup()")) + { + writer.AppendLine("if (_builder is { } b) return b;"); + writer.AppendLine("var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); + writer.AppendLine("_engine.AddSetup(setup);"); + writer.AppendLine($"return _builder = new {builderType}(setup);"); + } writer.AppendLine(); diff --git a/TUnit.Mocks/IMockObject.cs b/TUnit.Mocks/IMockObject.cs new file mode 100644 index 0000000000..29020ed4dd --- /dev/null +++ b/TUnit.Mocks/IMockObject.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; + +namespace TUnit.Mocks; + +/// +/// Implemented by generated mock impl classes to allow reverse-lookup +/// from a mock object to its wrapper without using +/// ConditionalWeakTable. Not intended for direct use. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public interface IMockObject +{ + /// + /// The wrapper for this mock object. + /// Set by the constructor. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + IMock? MockWrapper { get; set; } +} diff --git a/TUnit.Mocks/Mock.cs b/TUnit.Mocks/Mock.cs index 132c7eb29c..2553ea2e6d 100644 --- a/TUnit.Mocks/Mock.cs +++ b/TUnit.Mocks/Mock.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Runtime.CompilerServices; using TUnit.Mocks.Diagnostics; using TUnit.Mocks.Verification; @@ -10,10 +9,6 @@ namespace TUnit.Mocks; /// public static class Mock { - // Maps mock implementation objects back to their Mock wrappers. - // ConditionalWeakTable so mocks can be GC'd normally. - private static readonly ConditionalWeakTable _objectToMock = new(); - // The source generator registers factories via this method at module initialization time. // ConcurrentDictionary is used because module initializers from multiple assemblies // can run concurrently when test assemblies are loaded in parallel. @@ -31,21 +26,6 @@ public static class Mock // Separate registry for wrap mock factories that accept a real instance. private static readonly ConcurrentDictionary> _wrapFactories = new(); - /// - /// Registers the mapping from a mock implementation object to its wrapper. - /// Called from the constructor. Not intended for direct use. - /// - internal static void Register(object mockObject, IMock mockWrapper) - { -#if NET7_0_OR_GREATER - _objectToMock.AddOrUpdate(mockObject, mockWrapper); -#else - // ConditionalWeakTable.AddOrUpdate not available before .NET 7. - // Each mock object is unique (created by new), so Add will not throw. - _objectToMock.Add(mockObject, mockWrapper); -#endif - } - /// /// Retrieves the wrapper for a mock implementation object. /// Use this to access the mock wrapper from auto-mocked return values or any mocked object. @@ -60,13 +40,15 @@ internal static void Register(object mockObject, IMock mockWrapper) /// public static Mock Get(T mockedObject) where T : class { - if (_objectToMock.TryGetValue(mockedObject, out var mock)) + if (mockedObject is IMockObject { MockWrapper: { } wrapper }) { - if (mock is Mock typed) + if (wrapper is Mock typed) + { return typed; + } throw new InvalidOperationException( - $"The object is a mock of '{mock.GetType().GenericTypeArguments[0].Name}', not '{typeof(T).Name}'."); + $"The object is a mock of '{wrapper.GetType().GenericTypeArguments[0].Name}', not '{typeof(T).Name}'."); } throw new InvalidOperationException( @@ -81,7 +63,7 @@ public static Mock Get(T mockedObject) where T : class [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static void RegisterFactory(Func> factory) where T : class { - _factories[typeof(T)] = behavior => factory(behavior); + _factories[typeof(T)] = factory; } /// @@ -91,7 +73,7 @@ public static void RegisterFactory(Func> factory) where [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static void RegisterPartialFactory(Func> factory) where T : class { - _partialFactories[typeof(T)] = (behavior, args) => factory(behavior, args); + _partialFactories[typeof(T)] = factory; } /// @@ -111,7 +93,7 @@ public static void RegisterMultiFactory(string key, Func f [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static void RegisterDelegateFactory(Func> factory) where T : class { - _delegateFactories[typeof(T)] = behavior => factory(behavior); + _delegateFactories[typeof(T)] = factory; } /// diff --git a/TUnit.Mocks/MockEngine.cs b/TUnit.Mocks/MockEngine.cs index b431cd82ab..4edb00727e 100644 --- a/TUnit.Mocks/MockEngine.cs +++ b/TUnit.Mocks/MockEngine.cs @@ -22,9 +22,9 @@ internal static class MockCallSequence [EditorBrowsable(EditorBrowsableState.Never)] public sealed class MockEngine : IMockEngineAccess where T : class { - private readonly Dictionary> _setupsByMember = new(); private readonly Lock _setupLock = new(); - private readonly ConcurrentQueue _callHistory = new(); + private Dictionary>? _setupsByMember; + private ConcurrentQueue? _callHistory; private ConcurrentDictionary? _autoTrackValues; private ConcurrentQueue<(string EventName, bool IsSubscribe)>? _eventSubscriptions; @@ -119,9 +119,11 @@ public void AddSetup(MethodSetup setup) setup.RequiredState = PendingRequiredState; } - if (!_setupsByMember.TryGetValue(setup.MemberId, out var list)) + var dict = _setupsByMember ??= new(); + + if (!dict.TryGetValue(setup.MemberId, out var list)) { - _setupsByMember[setup.MemberId] = list = new(); + dict[setup.MemberId] = list = new(); } list.Add(setup); @@ -384,8 +386,13 @@ public bool TryHandleCallWithReturn(int memberId, string memberName, ob /// public IReadOnlyList GetCallsFor(int memberId) { + if (_callHistory is not { } history) + { + return []; + } + var result = new List(); - foreach (var record in _callHistory) + foreach (var record in history) { if (record.MemberId == memberId) { @@ -400,7 +407,7 @@ public IReadOnlyList GetCallsFor(int memberId) /// public IReadOnlyList GetAllCalls() { - return _callHistory.ToArray(); + return _callHistory?.ToArray() ?? []; } /// @@ -409,8 +416,13 @@ public IReadOnlyList GetAllCalls() [EditorBrowsable(EditorBrowsableState.Never)] public IReadOnlyList GetUnverifiedCalls() { + if (_callHistory is not { } history) + { + return []; + } + var result = new List(); - foreach (var record in _callHistory) + foreach (var record in history) { if (!record.IsVerified) { @@ -428,8 +440,13 @@ public IReadOnlyList GetSetups() { lock (_setupLock) { + if (_setupsByMember is not { } setups) + { + return []; + } + var all = new List(); - foreach (var list in _setupsByMember.Values) + foreach (var list in setups.Values) { all.AddRange(list); } @@ -465,11 +482,14 @@ public Diagnostics.MockDiagnostics GetDiagnostics() } var unmatchedCalls = new List(); - foreach (var call in _callHistory) + if (_callHistory is { } history) { - if (call.IsUnmatched) + foreach (var call in history) { - unmatchedCalls.Add(call); + if (call.IsUnmatched) + { + unmatchedCalls.Add(call); + } } } @@ -497,14 +517,12 @@ public void Reset() { lock (_setupLock) { - _setupsByMember.Clear(); + _setupsByMember = null; _currentState = null; PendingRequiredState = null; } - // Drain the queue - while (_callHistory.TryDequeue(out _)) { } - + Volatile.Write(ref _callHistory, null); Volatile.Write(ref _autoTrackValues, null); Volatile.Write(ref _eventSubscriptions, null); Volatile.Write(ref _onSubscribeCallbacks, null); @@ -591,7 +609,8 @@ private CallRecord RecordCall(int memberId, string memberName, object?[] args) { var seq = MockCallSequence.Next(); var record = new CallRecord(memberId, memberName, args, seq); - _callHistory.Enqueue(record); + var history = _callHistory ?? LazyInitializer.EnsureInitialized(ref _callHistory)!; + history.Enqueue(record); return record; } @@ -610,7 +629,7 @@ private void RaiseEventsForSetup(MethodSetup setup) { lock (_setupLock) { - if (!_setupsByMember.TryGetValue(memberId, out var setups)) + if (_setupsByMember is not { } setupDict || !setupDict.TryGetValue(memberId, out var setups)) { return (false, null, null); } diff --git a/TUnit.Mocks/MockMethodCall.cs b/TUnit.Mocks/MockMethodCall.cs index f624192f7f..d06b1e8d56 100644 --- a/TUnit.Mocks/MockMethodCall.cs +++ b/TUnit.Mocks/MockMethodCall.cs @@ -20,7 +20,7 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; - private readonly Lazy> _lazyBuilder; + private MethodSetupBuilder? _builder; [EditorBrowsable(EditorBrowsableState.Never)] public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) @@ -29,15 +29,15 @@ public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new Lazy>(() => - { - var setup = new MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new MethodSetupBuilder(setup); - }); } - private MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private MethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new MethodSetupBuilder(setup); + } // IMethodSetup implementation diff --git a/TUnit.Mocks/MockOfT.cs b/TUnit.Mocks/MockOfT.cs index 4f9550ca11..8675f9179c 100644 --- a/TUnit.Mocks/MockOfT.cs +++ b/TUnit.Mocks/MockOfT.cs @@ -27,7 +27,10 @@ public Mock(T mockObject, MockEngine engine) { _engine = engine; Object = mockObject; - Mock.Register(mockObject, this); + if (mockObject is IMockObject mockObj) + { + mockObj.MockWrapper = this; + } } /// diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index ac52557340..5b4ecc4a66 100644 --- a/TUnit.Mocks/Setup/MethodSetup.cs +++ b/TUnit.Mocks/Setup/MethodSetup.cs @@ -11,12 +11,14 @@ namespace TUnit.Mocks.Setup; public sealed class MethodSetup { private readonly IArgumentMatcher[] _matchers; - private readonly Lock _behaviorLock = new(); - private readonly List _behaviors = new(); - private readonly List _eventRaises = new(); + private Lock? _behaviorLock; + private List? _behaviors; + private List? _eventRaises; private Dictionary? _outRefAssignments; private int _callIndex; + private Lock EnsureBehaviorLock() => LazyInitializer.EnsureInitialized(ref _behaviorLock)!; + public int MemberId { get; } /// @@ -59,9 +61,10 @@ public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName public void AddBehavior(IBehavior behavior) { - lock (_behaviorLock) + lock (EnsureBehaviorLock()) { - _behaviors.Add(behavior); + var list = _behaviors ??= new(); + list.Add(behavior); } } @@ -80,16 +83,22 @@ public bool Matches(object?[] actualArgs) public void AddEventRaise(EventRaiseInfo raiseInfo) { - lock (_behaviorLock) + lock (EnsureBehaviorLock()) { - _eventRaises.Add(raiseInfo); + var list = _eventRaises ??= new(); + list.Add(raiseInfo); } } [EditorBrowsable(EditorBrowsableState.Never)] public IReadOnlyList GetEventRaises() { - lock (_behaviorLock) + if (_eventRaises is null) + { + return []; + } + + lock (_behaviorLock!) { return _eventRaises.ToList(); } @@ -118,7 +127,7 @@ public void ApplyCaptures(object?[] args) /// The value to assign. public void SetOutRefValue(int paramIndex, object? value) { - lock (_behaviorLock) + lock (EnsureBehaviorLock()) { _outRefAssignments ??= new Dictionary(); _outRefAssignments[paramIndex] = value; @@ -133,7 +142,12 @@ public void SetOutRefValue(int paramIndex, object? value) { get { - lock (_behaviorLock) + if (_behaviorLock is not { } lck) + { + return _outRefAssignments; + } + + lock (lck) { return _outRefAssignments; } @@ -156,15 +170,22 @@ public string[] GetMatcherDescriptions() public IBehavior? GetNextBehavior() { - lock (_behaviorLock) + if (_behaviors is null) + { + return null; + } + + lock (_behaviorLock!) { - if (_behaviors.Count == 0) + if (_behaviors is not { Count: > 0 } behaviors) + { return null; + } var index = _callIndex; if (_callIndex < int.MaxValue) _callIndex++; // Clamp to last behavior (last one repeats) - return _behaviors[Math.Min(index, _behaviors.Count - 1)]; + return behaviors[Math.Min(index, behaviors.Count - 1)]; } } } diff --git a/TUnit.Mocks/VoidMockMethodCall.cs b/TUnit.Mocks/VoidMockMethodCall.cs index 2df33a2ea5..2327cab71a 100644 --- a/TUnit.Mocks/VoidMockMethodCall.cs +++ b/TUnit.Mocks/VoidMockMethodCall.cs @@ -22,7 +22,7 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; - private readonly Lazy _lazyBuilder; + private VoidMethodSetupBuilder? _builder; [EditorBrowsable(EditorBrowsableState.Never)] public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) @@ -37,19 +37,19 @@ internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string membe _memberId = memberId; _memberName = memberName; _matchers = matchers; - _lazyBuilder = new Lazy(() => - { - var setup = new MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return new VoidMethodSetupBuilder(setup); - }); if (eagerRegister) { - _ = _lazyBuilder.Value; + _ = EnsureSetup(); } } - private VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + private VoidMethodSetupBuilder EnsureSetup() + { + if (_builder is { } b) return b; + var setup = new MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return _builder = new VoidMethodSetupBuilder(setup); + } // IVoidMethodSetup implementation From 70c8a49d9535055cb37dd58e7fdbbeb13b8f47fc Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:49:19 +0100 Subject: [PATCH 2/9] docs: add TUnit.Mocks projects to project structure guide --- .claude/docs/project-structure.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.claude/docs/project-structure.md b/.claude/docs/project-structure.md index d9042081aa..541a8089e9 100644 --- a/.claude/docs/project-structure.md +++ b/.claude/docs/project-structure.md @@ -50,6 +50,24 @@ | `TUnit.PublicAPI` | Snapshot tests for public API | | `TUnit.Templates.Tests` | Template instantiation tests | +## Mocking Framework + +| Project | Purpose | +|---------|---------| +| `TUnit.Mocks` | Source-generated mocking framework (core runtime: `MockEngine`, `Mock`, setup/verification) | +| `TUnit.Mocks.SourceGenerator` | Roslyn source generator that emits mock implementations and extension methods | +| `TUnit.Mocks.SourceGenerator.Roslyn414/44/47` | Roslyn version variants (link source from base generator) | +| `TUnit.Mocks.Analyzers` | Analyzers for mock usage correctness | +| `TUnit.Mocks.Assertions` | TUnit assertion integration for mocks | +| `TUnit.Mocks.Http` | HTTP mocking (`MockHttpHandler`, `MockHttpClient`) | +| `TUnit.Mocks.Logging` | `ILogger` mocking (`MockLogger`, `MockLogger`) | +| `TUnit.Mocks.Tests` | Runtime mock tests (672 tests) | +| `TUnit.Mocks.SourceGenerator.Tests` | Snapshot tests for generated code | +| `TUnit.Mocks.Analyzers.Tests` | Analyzer tests | +| `TUnit.Mocks.Http.Tests` | HTTP mock tests | +| `TUnit.Mocks.Logging.Tests` | Logging mock tests | +| `TUnit.Mocks.Benchmarks` | BenchmarkDotNet performance comparisons vs Moq/NSubstitute/FakeItEasy | + ## Performance & Benchmarking | Project | Purpose | From 9bbbb39870541353bd813491109c31a74bf577fb Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:52:27 +0100 Subject: [PATCH 3/9] fix: address PR review feedback on thread-safety and documentation - Add Volatile.Read for _behaviors/_eventRaises null checks in MethodSetup - Add Volatile.Read for _behaviorLock/_outRefAssignments in OutRefAssignments getter - Document single-threaded ownership invariant on MockMethodCall/VoidMockMethodCall - Document IMockObject public setter design rationale --- TUnit.Mocks/IMockObject.cs | 8 +++++++- TUnit.Mocks/MockMethodCall.cs | 1 + TUnit.Mocks/Setup/MethodSetup.cs | 11 ++++++----- TUnit.Mocks/VoidMockMethodCall.cs | 1 + 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/TUnit.Mocks/IMockObject.cs b/TUnit.Mocks/IMockObject.cs index 29020ed4dd..214e94ea20 100644 --- a/TUnit.Mocks/IMockObject.cs +++ b/TUnit.Mocks/IMockObject.cs @@ -7,12 +7,18 @@ namespace TUnit.Mocks; /// from a mock object to its wrapper without using /// ConditionalWeakTable. Not intended for direct use. /// +/// +/// This interface is public because it must be implemented by source-generated code +/// in the user's assembly. The setter is required because the wrapper is assigned by +/// 's constructor after the impl object is created. +/// hides it from IDE autocompletion. +/// [EditorBrowsable(EditorBrowsableState.Never)] public interface IMockObject { /// /// The wrapper for this mock object. - /// Set by the constructor. + /// Set once by the constructor; should not be reassigned. /// [EditorBrowsable(EditorBrowsableState.Never)] IMock? MockWrapper { get; set; } diff --git a/TUnit.Mocks/MockMethodCall.cs b/TUnit.Mocks/MockMethodCall.cs index d06b1e8d56..4ce341108d 100644 --- a/TUnit.Mocks/MockMethodCall.cs +++ b/TUnit.Mocks/MockMethodCall.cs @@ -20,6 +20,7 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; + // Not thread-safe: MockMethodCall instances are created per setup/verify call and owned by a single test thread. private MethodSetupBuilder? _builder; [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index 5b4ecc4a66..2ce36dabce 100644 --- a/TUnit.Mocks/Setup/MethodSetup.cs +++ b/TUnit.Mocks/Setup/MethodSetup.cs @@ -93,14 +93,14 @@ public void AddEventRaise(EventRaiseInfo raiseInfo) [EditorBrowsable(EditorBrowsableState.Never)] public IReadOnlyList GetEventRaises() { - if (_eventRaises is null) + if (Volatile.Read(ref _eventRaises) is null) { return []; } lock (_behaviorLock!) { - return _eventRaises.ToList(); + return _eventRaises!.ToList(); } } @@ -142,9 +142,10 @@ public void SetOutRefValue(int paramIndex, object? value) { get { - if (_behaviorLock is not { } lck) + var lck = Volatile.Read(ref _behaviorLock); + if (lck is null) { - return _outRefAssignments; + return Volatile.Read(ref _outRefAssignments); } lock (lck) @@ -170,7 +171,7 @@ public string[] GetMatcherDescriptions() public IBehavior? GetNextBehavior() { - if (_behaviors is null) + if (Volatile.Read(ref _behaviors) is null) { return null; } diff --git a/TUnit.Mocks/VoidMockMethodCall.cs b/TUnit.Mocks/VoidMockMethodCall.cs index 2327cab71a..b3dda03e30 100644 --- a/TUnit.Mocks/VoidMockMethodCall.cs +++ b/TUnit.Mocks/VoidMockMethodCall.cs @@ -22,6 +22,7 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; + // Not thread-safe: VoidMockMethodCall instances are created per setup/verify call and owned by a single test thread. private VoidMethodSetupBuilder? _builder; [EditorBrowsable(EditorBrowsableState.Never)] From c4cd837ead43dd376f949ec4b75f42f283801881 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:00:59 +0100 Subject: [PATCH 4/9] fix: use LazyInitializer.EnsureInitialized for exactly-once setup registration Replace manual null-check in EnsureSetup() with LazyInitializer.EnsureInitialized (ref bool + ref object? syncLock overload) to guarantee exactly-once AddSetup even under concurrent access. Applies to MockMethodCall, VoidMockMethodCall, and all source-generated *_MockCall classes. Also adds comment clarifying MockEngine._setupLock is a struct (no heap alloc). --- ...nheriting_Multiple_Interfaces.verified.txt | 18 ++--- .../Interface_With_Async_Methods.verified.txt | 50 +++++++------- .../Interface_With_Events.verified.txt | 18 ++--- ..._With_Keyword_Parameter_Names.verified.txt | 34 +++++----- .../Interface_With_Mixed_Members.verified.txt | 34 +++++----- ...ble_Reference_Type_Parameters.verified.txt | 66 +++++++++++-------- ...rface_With_Out_Ref_Parameters.verified.txt | 34 +++++----- ...rface_With_Overloaded_Methods.verified.txt | 66 +++++++++++-------- ...stract_Transitive_Return_Type.verified.txt | 18 ++--- .../Multi_Method_Interface.verified.txt | 34 +++++----- ...ple_Interface_With_One_Method.verified.txt | 18 ++--- .../Builders/MockMembersBuilder.cs | 35 +++++----- TUnit.Mocks/MockEngine.cs | 1 + TUnit.Mocks/MockMethodCall.cs | 17 ++--- TUnit.Mocks/VoidMockMethodCall.cs | 17 ++--- 15 files changed, 256 insertions(+), 204 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt index 855ee3ceb8..ef90b8fdde 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -109,6 +109,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IReadWriter_Write_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -119,13 +121,13 @@ namespace TUnit.Mocks.Generated _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; /// public IReadWriter_Write_M2_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt index 68045385da..6dd209e2bc 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -166,6 +166,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IAsyncService_GetValueAsync_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -175,13 +177,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IAsyncService_GetValueAsync_M0_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -254,6 +256,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IAsyncService_ComputeAsync_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -263,13 +267,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IAsyncService_ComputeAsync_M2_MockCall Returns(int value) { EnsureSetup().Returns(value); return this; } @@ -342,6 +346,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IAsyncService_InitializeAsync_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -352,13 +358,13 @@ namespace TUnit.Mocks.Generated _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; /// public IAsyncService_InitializeAsync_M3_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt index 6e8e905cea..9b5534c09c 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -144,6 +144,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal INotifier_Notify_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -154,13 +156,13 @@ namespace TUnit.Mocks.Generated _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; /// public INotifier_Notify_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt index f7760f1478..8851539fb3 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -120,6 +120,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal ITest_Test_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -130,13 +132,13 @@ namespace TUnit.Mocks.Generated _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; /// public ITest_Test_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } @@ -194,6 +196,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal ITest_Get_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -203,13 +207,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public ITest_Get_M1_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt index ac5874f1c8..8e56e6d8d4 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -190,6 +190,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IService_GetAsync_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -199,13 +201,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IService_GetAsync_M3_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -281,6 +283,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IService_Process_M4_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -291,13 +295,13 @@ namespace TUnit.Mocks.Generated _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; /// public IService_Process_M4_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt index 3437653b24..c55ec140c1 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -223,6 +223,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IFoo_Bar_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -233,13 +235,13 @@ namespace TUnit.Mocks.Generated _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; /// public IFoo_Bar_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } @@ -297,6 +299,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IFoo_GetValue_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -306,13 +310,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IFoo_GetValue_M1_MockCall Returns(string? value) { EnsureSetup().Returns(value); return this; } @@ -385,6 +389,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IFoo_Process_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -395,13 +401,13 @@ namespace TUnit.Mocks.Generated _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; /// public IFoo_Process_M2_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } @@ -459,6 +465,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IFoo_GetAsync_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -468,13 +476,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IFoo_GetAsync_M3_MockCall Returns(string? value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt index e2266e56d2..c2d2bbb4fe 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -111,6 +111,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IDictionary_TryGetValue_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -120,13 +122,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IDictionary_TryGetValue_M0_MockCall Returns(bool value) { EnsureSetup().Returns(value); return this; } @@ -203,6 +205,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IDictionary_Swap_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -213,13 +217,13 @@ namespace TUnit.Mocks.Generated _ = EnsureSetup(); } - private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; /// public IDictionary_Swap_M1_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt index 7d0139abda..7198477d00 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -203,6 +203,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IFormatter_Format_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -212,13 +214,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IFormatter_Format_M0_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -291,6 +293,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IFormatter_Format_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -300,13 +304,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IFormatter_Format_M1_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -379,6 +383,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IFormatter_Format_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -388,13 +394,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IFormatter_Format_M2_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } @@ -467,6 +473,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IFormatter_Format_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -476,13 +484,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IFormatter_Format_M3_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt index 2b37248eed..2545746aa2 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -104,6 +104,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IMyService_GetValue_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -113,13 +115,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IMyService_GetValue_M0_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt index 2e5c7cbe46..f7429cf72c 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -146,6 +146,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal ICalculator_Add_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -155,13 +157,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public ICalculator_Add_M0_MockCall Returns(int value) { EnsureSetup().Returns(value); return this; } @@ -234,6 +236,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal ICalculator_Subtract_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -243,13 +247,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public ICalculator_Subtract_M1_MockCall Returns(int value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt index edadb4f78a..dbb7feef84 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -87,6 +87,8 @@ namespace TUnit.Mocks.Generated private readonly string _memberName; private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; internal IGreeter_Greet_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) { @@ -96,13 +98,13 @@ namespace TUnit.Mocks.Generated _matchers = matchers; } - private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); - } + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; /// public IGreeter_Greet_M0_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 6f5e9e0327..0ed552cf81 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -154,6 +154,8 @@ private static void GenerateReturnUnifiedClass(CodeWriter writer, string wrapper writer.AppendLine("private readonly string _memberName;"); writer.AppendLine("private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers;"); writer.AppendLine($"private {builderType}? _builder;"); + writer.AppendLine("private bool _builderInitialized;"); + writer.AppendLine("private object? _builderLock;"); writer.AppendLine(); // Constructor @@ -168,14 +170,8 @@ private static void GenerateReturnUnifiedClass(CodeWriter writer, string wrapper writer.AppendLine(); - // EnsureSetup method - using (writer.Block($"private {builderType} EnsureSetup()")) - { - writer.AppendLine("if (_builder is { } b) return b;"); - writer.AppendLine("var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); - writer.AppendLine("_engine.AddSetup(setup);"); - writer.AppendLine($"return _builder = new {builderType}(setup);"); - } + // EnsureSetup method — uses LazyInitializer to guarantee exactly-once AddSetup + EmitEnsureSetup(writer, builderType); writer.AppendLine(); @@ -290,6 +286,8 @@ private static void GenerateVoidUnifiedClass(CodeWriter writer, string wrapperNa writer.AppendLine("private readonly string _memberName;"); writer.AppendLine("private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers;"); writer.AppendLine($"private {builderType}? _builder;"); + writer.AppendLine("private bool _builderInitialized;"); + writer.AppendLine("private object? _builderLock;"); writer.AppendLine(); // Constructor — eagerly registers because void methods @@ -306,14 +304,8 @@ private static void GenerateVoidUnifiedClass(CodeWriter writer, string wrapperNa writer.AppendLine(); - // EnsureSetup method - using (writer.Block($"private {builderType} EnsureSetup()")) - { - writer.AppendLine("if (_builder is { } b) return b;"); - writer.AppendLine("var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); - writer.AppendLine("_engine.AddSetup(setup);"); - writer.AppendLine($"return _builder = new {builderType}(setup);"); - } + // EnsureSetup method — uses LazyInitializer to guarantee exactly-once AddSetup + EmitEnsureSetup(writer, builderType); writer.AppendLine(); @@ -867,4 +859,15 @@ private static string GetArgParameterList(MockMemberModel method, bool includeRe return string.Join(", ", parts); } + private static void EmitEnsureSetup(CodeWriter writer, string builderType) + { + writer.AppendLine($"private {builderType} EnsureSetup() =>"); + writer.AppendLine($" global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () =>"); + writer.AppendLine(" {"); + writer.AppendLine(" var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); + writer.AppendLine(" _engine.AddSetup(setup);"); + writer.AppendLine($" return new {builderType}(setup);"); + writer.AppendLine(" })!;"); + } + } diff --git a/TUnit.Mocks/MockEngine.cs b/TUnit.Mocks/MockEngine.cs index 4edb00727e..527d689a40 100644 --- a/TUnit.Mocks/MockEngine.cs +++ b/TUnit.Mocks/MockEngine.cs @@ -22,6 +22,7 @@ internal static class MockCallSequence [EditorBrowsable(EditorBrowsableState.Never)] public sealed class MockEngine : IMockEngineAccess where T : class { + // Lock is a struct in .NET 9+; no heap alloc — stored inline in the MockEngine object. private readonly Lock _setupLock = new(); private Dictionary>? _setupsByMember; private ConcurrentQueue? _callHistory; diff --git a/TUnit.Mocks/MockMethodCall.cs b/TUnit.Mocks/MockMethodCall.cs index 4ce341108d..0faeac9627 100644 --- a/TUnit.Mocks/MockMethodCall.cs +++ b/TUnit.Mocks/MockMethodCall.cs @@ -20,8 +20,9 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; - // Not thread-safe: MockMethodCall instances are created per setup/verify call and owned by a single test thread. private MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; [EditorBrowsable(EditorBrowsableState.Never)] public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) @@ -32,13 +33,13 @@ public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, _matchers = matchers; } - private MethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new MethodSetupBuilder(setup); - } + private MethodSetupBuilder EnsureSetup() => + LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new MethodSetupBuilder(setup); + })!; // IMethodSetup implementation diff --git a/TUnit.Mocks/VoidMockMethodCall.cs b/TUnit.Mocks/VoidMockMethodCall.cs index b3dda03e30..c2bf80012c 100644 --- a/TUnit.Mocks/VoidMockMethodCall.cs +++ b/TUnit.Mocks/VoidMockMethodCall.cs @@ -22,8 +22,9 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; - // Not thread-safe: VoidMockMethodCall instances are created per setup/verify call and owned by a single test thread. private VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; [EditorBrowsable(EditorBrowsableState.Never)] public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) @@ -44,13 +45,13 @@ internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string membe } } - private VoidMethodSetupBuilder EnsureSetup() - { - if (_builder is { } b) return b; - var setup = new MethodSetup(_memberId, _matchers, _memberName); - _engine.AddSetup(setup); - return _builder = new VoidMethodSetupBuilder(setup); - } + private VoidMethodSetupBuilder EnsureSetup() => + LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new VoidMethodSetupBuilder(setup); + })!; // IVoidMethodSetup implementation From 376f438ee95d076829ed1de62ea11f32cc1642aa Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:08:24 +0100 Subject: [PATCH 5/9] fix: simplify RecordCall, remove incorrect comment, cache event raise snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove redundant null-check before LazyInitializer in RecordCall (it has its own fast-path internally; the extra branch was pure overhead) - Remove incorrect comment claiming Lock is a struct (it's a sealed class) - Cache GetEventRaises snapshot to avoid List.ToList() allocation on every matched call — events are only configured during setup so the snapshot is invalidated only on AddEventRaise --- TUnit.Mocks/MockEngine.cs | 4 +--- TUnit.Mocks/Setup/MethodSetup.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/TUnit.Mocks/MockEngine.cs b/TUnit.Mocks/MockEngine.cs index 527d689a40..9229349361 100644 --- a/TUnit.Mocks/MockEngine.cs +++ b/TUnit.Mocks/MockEngine.cs @@ -22,7 +22,6 @@ internal static class MockCallSequence [EditorBrowsable(EditorBrowsableState.Never)] public sealed class MockEngine : IMockEngineAccess where T : class { - // Lock is a struct in .NET 9+; no heap alloc — stored inline in the MockEngine object. private readonly Lock _setupLock = new(); private Dictionary>? _setupsByMember; private ConcurrentQueue? _callHistory; @@ -610,8 +609,7 @@ private CallRecord RecordCall(int memberId, string memberName, object?[] args) { var seq = MockCallSequence.Next(); var record = new CallRecord(memberId, memberName, args, seq); - var history = _callHistory ?? LazyInitializer.EnsureInitialized(ref _callHistory)!; - history.Enqueue(record); + LazyInitializer.EnsureInitialized(ref _callHistory)!.Enqueue(record); return record; } diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index 2ce36dabce..deef8ba524 100644 --- a/TUnit.Mocks/Setup/MethodSetup.cs +++ b/TUnit.Mocks/Setup/MethodSetup.cs @@ -14,6 +14,7 @@ public sealed class MethodSetup private Lock? _behaviorLock; private List? _behaviors; private List? _eventRaises; + private EventRaiseInfo[]? _eventRaisesSnapshot; private Dictionary? _outRefAssignments; private int _callIndex; @@ -87,6 +88,7 @@ public void AddEventRaise(EventRaiseInfo raiseInfo) { var list = _eventRaises ??= new(); list.Add(raiseInfo); + _eventRaisesSnapshot = null; } } @@ -98,9 +100,14 @@ public IReadOnlyList GetEventRaises() return []; } + if (Volatile.Read(ref _eventRaisesSnapshot) is { } snapshot) + { + return snapshot; + } + lock (_behaviorLock!) { - return _eventRaises!.ToList(); + return _eventRaisesSnapshot = _eventRaises!.ToArray(); } } From 23e3e878cd126c268f3fbc6e8a18a4a8fb8488ac Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:11:58 +0100 Subject: [PATCH 6/9] fix: use Volatile.Read for _behaviorLock in read paths Use Volatile.Read(ref _behaviorLock) instead of bare _behaviorLock! in GetEventRaises and GetNextBehavior to make the memory ordering explicit and eliminate any theoretical stale-read concern on weakly-ordered architectures. --- TUnit.Mocks/Setup/MethodSetup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index deef8ba524..c934c77d7d 100644 --- a/TUnit.Mocks/Setup/MethodSetup.cs +++ b/TUnit.Mocks/Setup/MethodSetup.cs @@ -105,7 +105,7 @@ public IReadOnlyList GetEventRaises() return snapshot; } - lock (_behaviorLock!) + lock (Volatile.Read(ref _behaviorLock)!) { return _eventRaisesSnapshot = _eventRaises!.ToArray(); } @@ -183,7 +183,7 @@ public string[] GetMatcherDescriptions() return null; } - lock (_behaviorLock!) + lock (Volatile.Read(ref _behaviorLock)!) { if (_behaviors is not { Count: > 0 } behaviors) { From 0661e02cdff11203e3ef7b09d7fdd14021dde1c7 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:16:00 +0100 Subject: [PATCH 7/9] fix: use Volatile.Read for _callHistory in query paths, use CodeWriter indent API - Wrap _callHistory reads in GetCallsFor, GetAllCalls, GetUnverifiedCalls, and GetDiagnostics with Volatile.Read for ARM memory model correctness - Replace hardcoded spaces in EmitEnsureSetup with CodeWriter's IncreaseIndent/OpenBrace/CloseBrace API for resilient indentation --- .../Builders/MockMembersBuilder.cs | 14 ++++++++------ TUnit.Mocks/MockEngine.cs | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 0ed552cf81..c3339ed0a2 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -862,12 +862,14 @@ private static string GetArgParameterList(MockMemberModel method, bool includeRe private static void EmitEnsureSetup(CodeWriter writer, string builderType) { writer.AppendLine($"private {builderType} EnsureSetup() =>"); - writer.AppendLine($" global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () =>"); - writer.AppendLine(" {"); - writer.AppendLine(" var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); - writer.AppendLine(" _engine.AddSetup(setup);"); - writer.AppendLine($" return new {builderType}(setup);"); - writer.AppendLine(" })!;"); + writer.IncreaseIndent(); + writer.AppendLine($"global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () =>"); + writer.OpenBrace(); + writer.AppendLine("var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);"); + writer.AppendLine("_engine.AddSetup(setup);"); + writer.AppendLine($"return new {builderType}(setup);"); + writer.CloseBrace(")!;"); + writer.DecreaseIndent(); } } diff --git a/TUnit.Mocks/MockEngine.cs b/TUnit.Mocks/MockEngine.cs index 9229349361..2e69abad53 100644 --- a/TUnit.Mocks/MockEngine.cs +++ b/TUnit.Mocks/MockEngine.cs @@ -386,7 +386,7 @@ public bool TryHandleCallWithReturn(int memberId, string memberName, ob /// public IReadOnlyList GetCallsFor(int memberId) { - if (_callHistory is not { } history) + if (Volatile.Read(ref _callHistory) is not { } history) { return []; } @@ -407,7 +407,7 @@ public IReadOnlyList GetCallsFor(int memberId) /// public IReadOnlyList GetAllCalls() { - return _callHistory?.ToArray() ?? []; + return Volatile.Read(ref _callHistory)?.ToArray() ?? []; } /// @@ -416,7 +416,7 @@ public IReadOnlyList GetAllCalls() [EditorBrowsable(EditorBrowsableState.Never)] public IReadOnlyList GetUnverifiedCalls() { - if (_callHistory is not { } history) + if (Volatile.Read(ref _callHistory) is not { } history) { return []; } @@ -482,7 +482,7 @@ public Diagnostics.MockDiagnostics GetDiagnostics() } var unmatchedCalls = new List(); - if (_callHistory is { } history) + if (Volatile.Read(ref _callHistory) is { } history) { foreach (var call in history) { From 9de2cfb54ceb1f7f6badfcb46e8ed8c2ebeee0bc Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:21:51 +0100 Subject: [PATCH 8/9] fix: use EnsureBehaviorLock() in read paths, simplify OutRefAssignments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Volatile.Read(ref _behaviorLock)! with EnsureBehaviorLock() in GetEventRaises and GetNextBehavior — makes the non-null invariant explicit and defensive rather than relying on field ordering - Simplify OutRefAssignments null-lock fast path to return null directly (if lock is null, no writer has run, so field is guaranteed null) --- TUnit.Mocks/Setup/MethodSetup.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index c934c77d7d..dfb3d37822 100644 --- a/TUnit.Mocks/Setup/MethodSetup.cs +++ b/TUnit.Mocks/Setup/MethodSetup.cs @@ -105,7 +105,7 @@ public IReadOnlyList GetEventRaises() return snapshot; } - lock (Volatile.Read(ref _behaviorLock)!) + lock (EnsureBehaviorLock()) { return _eventRaisesSnapshot = _eventRaises!.ToArray(); } @@ -152,7 +152,7 @@ public void SetOutRefValue(int paramIndex, object? value) var lck = Volatile.Read(ref _behaviorLock); if (lck is null) { - return Volatile.Read(ref _outRefAssignments); + return null; } lock (lck) @@ -183,7 +183,7 @@ public string[] GetMatcherDescriptions() return null; } - lock (Volatile.Read(ref _behaviorLock)!) + lock (EnsureBehaviorLock()) { if (_behaviors is not { Count: > 0 } behaviors) { From 8615fb7cbc7a39a278cc1278679274ba33ccc52e Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:26:19 +0100 Subject: [PATCH 9/9] fix: add double-check pattern in GetEventRaises snapshot cache --- TUnit.Mocks/Setup/MethodSetup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index dfb3d37822..0370b2f8c2 100644 --- a/TUnit.Mocks/Setup/MethodSetup.cs +++ b/TUnit.Mocks/Setup/MethodSetup.cs @@ -107,7 +107,7 @@ public IReadOnlyList GetEventRaises() lock (EnsureBehaviorLock()) { - return _eventRaisesSnapshot = _eventRaises!.ToArray(); + return _eventRaisesSnapshot ??= _eventRaises!.ToArray(); } }