Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,24 @@ internal static class MockMembersBuilder
private const int MaxTypedParams = 8;
private const int MaxFuncOverloadParams = 4;

// "Object" collides with Mock<T>.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<string> MockMemberNames = new(System.StringComparer.Ordinal)
{
"Object",
"GetHashCode", "GetType", "ToString", "Equals"
};

// Members on object/Mock<T> 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<string, string> ObjectMemberDisambiguations = new(System.StringComparer.Ordinal)
{
{ "Equals", "EqualsOf" },
{ "GetHashCode", "GetHashCodeOf" },
{ "ToString", "ToStringOf" },
{ "GetType", "GetTypeOf" },
};

public static string Build(MockTypeModel model)
Expand Down Expand Up @@ -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)
{
Expand Down
46 changes: 40 additions & 6 deletions TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,17 @@ public class DoubleInterfaceExplicit : IGetIdInt, IGetIdString
public virtual int OwnMember() => 0;
}

// ─── T8 SKIPPED. Self-referential IEquatable<T> — `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<T> — `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<SelfEquatable>
{
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 ────────────────────────────────────────────────

Expand Down Expand Up @@ -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<SelfEquatable> 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 ──

Expand Down
Loading