From 932d2538c34fc00a34cf20b032ddfb88e63ddea6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:33:49 +0100 Subject: [PATCH 1/3] fix(mocks): disambiguate IEquatable.Equals from object.Equals (#5675) Generated extension methods named Equals, GetHashCode, ToString, or GetType on Mock were unreachable: instance methods on object always win overload resolution against extensions, so `mock.Equals(other).Returns(true)` failed with CS1061 ("'bool' does not contain a definition for 'Returns'"). Rename the generated setup helpers to EqualsOf/GetHashCodeOf/ToStringOf/ GetTypeOf so the typed setup stays reachable. The "Object" rename to "Object_" (Mock.Object property collision) is unchanged. Adds T8 in KitchenSinkEdgeCasesTests covering the IEquatable scenario end-to-end: setup, direct invocation, interface-cast invocation, and WasCalled/WasNeverCalled tracking. --- .../Builders/MockMembersBuilder.cs | 22 ++++++++- .../KitchenSinkEdgeCasesTests.cs | 46 ++++++++++++++++--- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 43fce63d63..e124e0f6bf 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -16,10 +16,24 @@ internal static class MockMembersBuilder private const int MaxTypedParams = 8; private const int MaxFuncOverloadParams = 4; + // "Object" collides with Mock.Object (the property exposing the underlying instance); + // we rename a generated member named Object to Object_ to avoid the property/method clash. private static readonly HashSet MockMemberNames = new(System.StringComparer.Ordinal) { "Object", - "GetHashCode", "GetType", "ToString", "Equals" + }; + + // Members on object/Mock that, when re-emitted as a generated extension method, + // would lose overload resolution to the base instance method (e.g. mock.Equals(other) + // resolves to object.Equals(object?) rather than the typed extension). We give the + // generated setup a disambiguating name so it remains reachable: Equals -> EqualsOf, + // GetHashCode -> GetHashCodeOf, ToString -> ToStringOf, GetType -> GetTypeOf. + private static readonly Dictionary ObjectMemberDisambiguations = new(System.StringComparer.Ordinal) + { + { "Equals", "EqualsOf" }, + { "GetHashCode", "GetHashCodeOf" }, + { "ToString", "ToStringOf" }, + { "GetType", "GetTypeOf" }, }; public static string Build(MockTypeModel model) @@ -137,7 +151,11 @@ private static string GetWrapperName(string safeName, MockMemberModel method) => $"{safeName}_{method.Name}_M{method.MemberId}_MockCall"; private static string GetSafeMemberName(string name) - => MockMemberNames.Contains(name) ? name + "_" : name; + { + if (ObjectMemberDisambiguations.TryGetValue(name, out var renamed)) + return renamed; + return MockMemberNames.Contains(name) ? name + "_" : name; + } private static string GetCombinedTypeParameterList(MockTypeModel model, MockMemberModel method) { diff --git a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs index 0421541d1c..89a7bb4636 100644 --- a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs +++ b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs @@ -101,11 +101,17 @@ public class DoubleInterfaceExplicit : IGetIdInt, IGetIdString public virtual int OwnMember() => 0; } -// ─── T8 SKIPPED. Self-referential IEquatable — `mock.Equals(...)` resolves -// to object.Equals via extension-method dispatch rather than to the -// generator-emitted setup extension. Separate design limitation: would -// require either renaming the extension or generating a disambiguating -// helper (e.g. `mock.EqualsOf(...)`). +// ─── T8. Self-referential IEquatable — `mock.Equals(...)` would resolve to +// object.Equals via overload-resolution (extension methods can't beat instance +// methods on object). Generator emits a disambiguating `EqualsOf(...)` helper +// so the typed setup is reachable. + +public class SelfEquatable : IEquatable +{ + public virtual bool Equals(SelfEquatable? other) => ReferenceEquals(this, other); + public override bool Equals(object? obj) => obj is SelfEquatable s && Equals(s); + public override int GetHashCode() => 0; +} // ─── T9. Nullable value types ──────────────────────────────────────────────── @@ -334,7 +340,35 @@ public async Task T7_Two_Interfaces_Same_Name_Different_Returns() mock.OwnMember().WasCalled(Times.Once); } - // T8 test elided — see the SKIPPED note above the type declarations. + // ── T8 ── + + [Test] + public async Task T8_Self_Referential_IEquatable_Mockable() + { + var mock = SelfEquatable.Mock(); + var other = new SelfEquatable(); + var unrelated = new SelfEquatable(); + + // Setup via the disambiguated helper. Returns(...) must be reachable on the result. + mock.EqualsOf(other).Returns(true); + mock.EqualsOf(unrelated).Returns(false); + + // Direct call routes through the generated impl's Equals override to the engine. + await Assert.That(mock.Object.Equals(other)).IsTrue(); + await Assert.That(mock.Object.Equals(unrelated)).IsFalse(); + + // Interface-cast path resolves to the same underlying setup. + IEquatable asInterface = mock.Object; + await Assert.That(asInterface.Equals(other)).IsTrue(); + await Assert.That(asInterface.Equals(unrelated)).IsFalse(); + + // Verification: each setup tracks both the direct and interface-cast invocations. + mock.EqualsOf(other).WasCalled(Times.Exactly(2)); + mock.EqualsOf(unrelated).WasCalled(Times.Exactly(2)); + + var third = new SelfEquatable(); + mock.EqualsOf(third).WasNeverCalled(); + } // ── T9 ── From 6e68366ecc718bc3c9ccf5ef2af587bc1914acd5 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:55:47 +0100 Subject: [PATCH 2/3] chore(mocks): address review feedback on #5675 - Expand WHY comments in MockMembersBuilder to contrast the two distinct shadowing problems (Object property vs. inherited object instance methods) and explain why they get different fixes (underscore suffix vs. "Of" suffix). - Extend T8 to also cover GetHashCodeOf and ToStringOf. Document inline that GetTypeOf is unreachable because GetType is not virtual on object. --- .../Builders/MockMembersBuilder.cs | 18 +++++++++++------- TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index e124e0f6bf..d75e30b0cf 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -16,18 +16,22 @@ internal static class MockMembersBuilder private const int MaxTypedParams = 8; private const int MaxFuncOverloadParams = 4; - // "Object" collides with Mock.Object (the property exposing the underlying instance); - // we rename a generated member named Object to Object_ to avoid the property/method clash. + // Two distinct shadowing problems below — different fixes because the kinds collide differently. + + // "Object" clashes with the Mock.Object PROPERTY (member-kind collision: property vs. + // generated method/property). A trailing underscore is enough since nothing on object + // is named "Object_". private static readonly HashSet MockMemberNames = new(System.StringComparer.Ordinal) { "Object", }; - // Members on object/Mock that, when re-emitted as a generated extension method, - // would lose overload resolution to the base instance method (e.g. mock.Equals(other) - // resolves to object.Equals(object?) rather than the typed extension). We give the - // generated setup a disambiguating name so it remains reachable: Equals -> EqualsOf, - // GetHashCode -> GetHashCodeOf, ToString -> ToStringOf, GetType -> GetTypeOf. + // Equals/GetHashCode/ToString/GetType clash with inherited object INSTANCE METHODS. + // C# overload resolution always prefers an instance method on the receiver over an + // extension method, so a generated `Equals` extension is unreachable (mock.Equals(...) + // binds to object.Equals and returns bool). A trailing-underscore rename would also be + // reachable, but we use an "Of" suffix to read naturally at the call site + // (mock.EqualsOf(other).Returns(true)). private static readonly Dictionary ObjectMemberDisambiguations = new(System.StringComparer.Ordinal) { { "Equals", "EqualsOf" }, diff --git a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs index 89a7bb4636..85195910a6 100644 --- a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs +++ b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs @@ -104,13 +104,17 @@ public class DoubleInterfaceExplicit : IGetIdInt, IGetIdString // ─── T8. Self-referential IEquatable — `mock.Equals(...)` would resolve to // object.Equals via overload-resolution (extension methods can't beat instance // methods on object). Generator emits a disambiguating `EqualsOf(...)` helper -// so the typed setup is reachable. +// so the typed setup is reachable. Same disambiguation applies to GetHashCode +// and ToString. (GetType is not virtual on object, so it can never be overridden +// and the GetTypeOf helper is unreachable in practice — kept symmetrically for +// completeness only.) public class SelfEquatable : IEquatable { public virtual bool Equals(SelfEquatable? other) => ReferenceEquals(this, other); public override bool Equals(object? obj) => obj is SelfEquatable s && Equals(s); public override int GetHashCode() => 0; + public override string ToString() => "base"; } // ─── T9. Nullable value types ──────────────────────────────────────────────── @@ -368,6 +372,17 @@ public async Task T8_Self_Referential_IEquatable_Mockable() var third = new SelfEquatable(); mock.EqualsOf(third).WasNeverCalled(); + + // GetHashCodeOf / ToStringOf — same disambiguation pattern. + // GetType is not virtual on object so cannot be exercised; see class-level note. + mock.GetHashCodeOf().Returns(7); + mock.ToStringOf().Returns("mocked"); + + await Assert.That(mock.Object.GetHashCode()).IsEqualTo(7); + await Assert.That(mock.Object.ToString()).IsEqualTo("mocked"); + + mock.GetHashCodeOf().WasCalled(Times.Once); + mock.ToStringOf().WasCalled(Times.Once); } // ── T9 ── From f4a63be2272a1e4d9250edc431b3ddb9b58a2aba Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:03:48 +0100 Subject: [PATCH 3/3] chore(mocks): add *Of snapshot test, drop unreachable GetTypeOf Round-2 review feedback on PR #5680: - Add SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf snapshot in TUnit.Mocks.SourceGenerator.Tests so the disambiguated extension helpers (EqualsOf / GetHashCodeOf / ToStringOf) have explicit source-gen coverage instead of relying solely on the runtime T8 test. - Remove the GetType -> GetTypeOf entry from ObjectMemberDisambiguations. GetType is non-virtual on object so it can never be overridden, which made the generated helper dead code. Update related comments accordingly. --- .../MockGeneratorTests.cs | 32 ++ ...lsOf_GetHashCodeOf_ToStringOf.verified.txt | 336 ++++++++++++++++++ .../Builders/MockMembersBuilder.cs | 6 +- .../KitchenSinkEdgeCasesTests.cs | 5 +- 4 files changed, 374 insertions(+), 5 deletions(-) create mode 100644 TUnit.Mocks.SourceGenerator.Tests/Snapshots/SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt diff --git a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs index 2e7891ae62..ad61c62f9c 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs +++ b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs @@ -1347,6 +1347,38 @@ void M() return VerifyGeneratorOutput(source); } + [Test] + public Task SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf() + { + // Regression for #5675: self-referential IEquatable together with + // overrides of GetHashCode/ToString must produce disambiguated extension + // helpers (EqualsOf / GetHashCodeOf / ToStringOf) on the generated mock, + // because C# overload resolution always prefers the inherited object + // instance methods over an extension named the same. + var source = """ + using System; + using TUnit.Mocks; + + public class SelfEquatableSnapshot : IEquatable + { + public virtual bool Equals(SelfEquatableSnapshot? other) => ReferenceEquals(this, other); + public override bool Equals(object? obj) => obj is SelfEquatableSnapshot s && Equals(s); + public override int GetHashCode() => 0; + public override string ToString() => "base"; + } + + public class TestUsage + { + void M() + { + var mock = Mock.Of(); + } + } + """; + + return VerifyGeneratorOutput(source); + } + [Test] public Task Interface_Inheriting_Nested_Generic_IEnumerable() { diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt new file mode 100644 index 0000000000..72e6bbdcba --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt @@ -0,0 +1,336 @@ +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + file sealed class SelfEquatableSnapshotMockImpl : global::SelfEquatableSnapshot, 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 SelfEquatableSnapshotMockImpl(global::TUnit.Mocks.MockEngine engine) : base() + { + _engine = engine; + } + + public override bool Equals(global::SelfEquatableSnapshot? other) + { + if (_engine.TryHandleCallWithReturn(0, "Equals", other, default, out var __result)) + { + return __result; + } + return base.Equals(other); + } + + public override bool Equals(object? obj) + { + if (_engine.TryHandleCallWithReturn(1, "Equals", obj, default, out var __result)) + { + return __result; + } + return base.Equals(obj); + } + + public override int GetHashCode() + { + if (_engine.TryHandleCallWithReturn(2, "GetHashCode", global::System.Array.Empty(), default, out var __result)) + { + return __result; + } + return base.GetHashCode(); + } + + public override string ToString() + { + if (_engine.TryHandleCallWithReturn(3, "ToString", global::System.Array.Empty(), "", out var __result)) + { + return __result; + } + return base.ToString(); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } + } + + file static class SelfEquatableSnapshotPartialMockFactory + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory(Create); + } + + private static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + var engine = new global::TUnit.Mocks.MockEngine(behavior); + var impl = new SelfEquatableSnapshotMockImpl(engine); + engine.Raisable = impl; + var mock = new global::TUnit.Mocks.Mock(impl, engine); + return mock; + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class SelfEquatableSnapshot_MockMemberExtensions + { + public static SelfEquatableSnapshot_Equals_M0_MockCall EqualsOf(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg other) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { other.Matcher }; + return new SelfEquatableSnapshot_Equals_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Equals", matchers); + } + + public static SelfEquatableSnapshot_Equals_M0_MockCall EqualsOf(this global::TUnit.Mocks.Mock mock, global::System.Func other) + { + global::TUnit.Mocks.Arguments.Arg __fa_other = other; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_other.Matcher }; + return new SelfEquatableSnapshot_Equals_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Equals", matchers); + } + + public static SelfEquatableSnapshot_Equals_M1_MockCall EqualsOf(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg obj) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { obj.Matcher }; + return new SelfEquatableSnapshot_Equals_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Equals", matchers); + } + + public static SelfEquatableSnapshot_Equals_M1_MockCall EqualsOf(this global::TUnit.Mocks.Mock mock, global::System.Func obj) + { + global::TUnit.Mocks.Arguments.Arg __fa_obj = obj; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_obj.Matcher }; + return new SelfEquatableSnapshot_Equals_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Equals", matchers); + } + + public static global::TUnit.Mocks.MockMethodCall GetHashCodeOf(this global::TUnit.Mocks.Mock mock) + { + var matchers = global::System.Array.Empty(); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "GetHashCode", matchers); + } + + public static global::TUnit.Mocks.MockMethodCall ToStringOf(this global::TUnit.Mocks.Mock mock) + { + var matchers = global::System.Array.Empty(); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "ToString", matchers); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class SelfEquatableSnapshot_Equals_M0_MockCall : global::TUnit.Mocks.Verification.ICallVerification + { + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + + internal SelfEquatableSnapshot_Equals_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + } + + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + var existing = global::System.Threading.Volatile.Read(ref _builder); + if (existing is not null) return existing; + return EnsureSetupSlow(); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetupSlow() + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + var fresh = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + var prev = global::System.Threading.Interlocked.CompareExchange(ref _builder, fresh, null); + if (prev is not null) return prev; + // AddSetup runs only on the CAS winner. Setup is sequential in practice, + // so a concurrent loser observing the builder before registration is benign. + _engine.AddSetup(setup); + return fresh; + } + + /// + public SelfEquatableSnapshot_Equals_M0_MockCall Returns(bool value) { EnsureSetup().Returns(value); return this; } + /// + public SelfEquatableSnapshot_Equals_M0_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public SelfEquatableSnapshot_Equals_M0_MockCall ReturnsSequentially(params bool[] values) { EnsureSetup().ReturnsSequentially(values); return this; } + /// + public SelfEquatableSnapshot_Equals_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public SelfEquatableSnapshot_Equals_M0_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public SelfEquatableSnapshot_Equals_M0_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public SelfEquatableSnapshot_Equals_M0_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public SelfEquatableSnapshot_Equals_M0_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Configure a typed computed return value using the actual method parameters. + public SelfEquatableSnapshot_Equals_M0_MockCall Returns(global::System.Func factory) + { + EnsureSetup().Returns(args => factory((global::SelfEquatableSnapshot?)args[0])); + return this; + } + + /// Execute a typed callback using the actual method parameters. + public SelfEquatableSnapshot_Equals_M0_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(callback); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public SelfEquatableSnapshot_Equals_M0_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((global::SelfEquatableSnapshot?)args[0])); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class SelfEquatableSnapshot_Equals_M1_MockCall : global::TUnit.Mocks.Verification.ICallVerification + { + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + + internal SelfEquatableSnapshot_Equals_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + } + + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + var existing = global::System.Threading.Volatile.Read(ref _builder); + if (existing is not null) return existing; + return EnsureSetupSlow(); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetupSlow() + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + var fresh = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + var prev = global::System.Threading.Interlocked.CompareExchange(ref _builder, fresh, null); + if (prev is not null) return prev; + // AddSetup runs only on the CAS winner. Setup is sequential in practice, + // so a concurrent loser observing the builder before registration is benign. + _engine.AddSetup(setup); + return fresh; + } + + /// + public SelfEquatableSnapshot_Equals_M1_MockCall Returns(bool value) { EnsureSetup().Returns(value); return this; } + /// + public SelfEquatableSnapshot_Equals_M1_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public SelfEquatableSnapshot_Equals_M1_MockCall ReturnsSequentially(params bool[] values) { EnsureSetup().ReturnsSequentially(values); return this; } + /// + public SelfEquatableSnapshot_Equals_M1_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public SelfEquatableSnapshot_Equals_M1_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public SelfEquatableSnapshot_Equals_M1_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public SelfEquatableSnapshot_Equals_M1_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public SelfEquatableSnapshot_Equals_M1_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Configure a typed computed return value using the actual method parameters. + public SelfEquatableSnapshot_Equals_M1_MockCall Returns(global::System.Func factory) + { + EnsureSetup().Returns(args => factory((object?)args[0])); + return this; + } + + /// Execute a typed callback using the actual method parameters. + public SelfEquatableSnapshot_Equals_M1_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(callback); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public SelfEquatableSnapshot_Equals_M1_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((object?)args[0])); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks +{ + public static class SelfEquatableSnapshot_MockStaticExtension + { + extension(global::SelfEquatableSnapshot _) + { + public static global::TUnit.Mocks.Mock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose) + { + return global::TUnit.Mocks.Mock.Of(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index d75e30b0cf..ac2d121472 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -26,18 +26,20 @@ internal static class MockMembersBuilder "Object", }; - // Equals/GetHashCode/ToString/GetType clash with inherited object INSTANCE METHODS. + // Equals/GetHashCode/ToString clash with inherited object INSTANCE METHODS. // C# overload resolution always prefers an instance method on the receiver over an // extension method, so a generated `Equals` extension is unreachable (mock.Equals(...) // binds to object.Equals and returns bool). A trailing-underscore rename would also be // reachable, but we use an "Of" suffix to read naturally at the call site // (mock.EqualsOf(other).Returns(true)). + // GetType is intentionally omitted: it is non-virtual on object and therefore + // cannot be overridden or shadowed in a meaningful way, so any generated helper + // would be dead code. private static readonly Dictionary ObjectMemberDisambiguations = new(System.StringComparer.Ordinal) { { "Equals", "EqualsOf" }, { "GetHashCode", "GetHashCodeOf" }, { "ToString", "ToStringOf" }, - { "GetType", "GetTypeOf" }, }; public static string Build(MockTypeModel model) diff --git a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs index 85195910a6..c6836dadb6 100644 --- a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs +++ b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs @@ -105,9 +105,8 @@ public class DoubleInterfaceExplicit : IGetIdInt, IGetIdString // object.Equals via overload-resolution (extension methods can't beat instance // methods on object). Generator emits a disambiguating `EqualsOf(...)` helper // so the typed setup is reachable. Same disambiguation applies to GetHashCode -// and ToString. (GetType is not virtual on object, so it can never be overridden -// and the GetTypeOf helper is unreachable in practice — kept symmetrically for -// completeness only.) +// and ToString. (GetType is non-virtual on object so it cannot be overridden; +// no helper is generated for it.) public class SelfEquatable : IEquatable {