diff --git a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 723f1a08b6..594a9b283a 100644 --- a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -588,7 +588,7 @@ private static void GenerateInterfaceMethod(CodeWriter writer, MockMemberModel m var signatureReturnType = (method.IsVoid && !method.IsAsync) ? "void" : method.ReturnType; var paramList = GetParameterList(method); var typeParams = GetTypeParameterList(method); - var constraints = GetConstraintClauses(method); + var constraints = GetConstraintClauses(method, forExplicitImplementation: method.ExplicitInterfaceName is not null); writer.AppendLineIfNotEmpty(method.ObsoleteAttribute); @@ -599,7 +599,7 @@ private static void GenerateInterfaceMethod(CodeWriter writer, MockMemberModel m // Return type is compatible (e.g. IEnumerable.GetEnumerator → IEnumerable.GetEnumerator) // — delegate to the public method. var argPassList = GetArgPassList(method); - writer.AppendLine($"{signatureReturnType} {method.ExplicitInterfaceName}.{EscapeIdentifier(method.Name)}{typeParams}({paramList}){constraints} => {EscapeIdentifier(method.Name)}({argPassList});"); + writer.AppendLine($"{signatureReturnType} {method.ExplicitInterfaceName}.{EscapeIdentifier(method.Name)}{typeParams}({paramList}){constraints} => {EscapeIdentifier(method.Name)}{typeParams}({argPassList});"); } else { diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs new file mode 100644 index 0000000000..50209b1337 --- /dev/null +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs @@ -0,0 +1,50 @@ +namespace TUnit.Mocks.SourceGenerator.Tests; + +/// +/// Regression: https://github.com/thomhurst/TUnit/issues/6670 +/// A derived interface can hide a generic base method with a different return type. The base +/// slot then needs an explicit implementation, which cannot repeat inherited constraints such +/// as notnull (CS0460). +/// +public class Issue6670Tests : SnapshotTestBase +{ + private const string Source = """ + using System.Collections.Generic; + using TUnit.Mocks; + + public interface ITest : ITestParent + { + new IList Get() where T : notnull; + } + + public interface ITestParent + { + IEnumerable Get() where T : notnull; + } + + public class TestUsage + { + void M() + { + var mock = ITest.Mock(); + } + } + """; + + [Test] + public async Task Hidden_Generic_Interface_Method_Does_Not_Repeat_Constraints_On_Explicit_Implementation() + { + var errors = GetGeneratedCompilationErrors(Source); + var genericImplementationErrors = errors + .Where(error => error.Id is "CS0411" or "CS0460") + .ToList(); + + await Assert.That(genericImplementationErrors).IsEmpty(); + } + + [Test] + public Task Hidden_Generic_Interface_Method_Generation_Snapshot() + { + return VerifyGeneratorOutput(Source); + } +} diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Hidden_Generic_Interface_Method_Generation_Snapshot.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Hidden_Generic_Interface_Method_Generation_Snapshot.verified.txt new file mode 100644 index 0000000000..1ba4548b22 --- /dev/null +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Hidden_Generic_Interface_Method_Generation_Snapshot.verified.txt @@ -0,0 +1,172 @@ +// +#pragma warning disable +#nullable enable + +public sealed class ITestMock : global::TUnit.Mocks.Mock, global::ITest +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal ITestMock(global::ITest mockObject, global::TUnit.Mocks.MockEngine engine) + : base(mockObject, engine) { } + + global::System.Collections.Generic.IList global::ITest.Get() => Object.Get(); + + global::System.Collections.Generic.IEnumerable global::ITestParent.Get() => ((global::ITestParent)Object).Get(); +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +file sealed class ITestMockImpl : 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 ITestMockImpl(global::TUnit.Mocks.MockEngine engine) + { + _engine = engine; + } + + public global::System.Collections.Generic.IList Get() where T : notnull + { + return _engine.HandleCallWithReturn>(0, "Get", global::System.Array.Empty(), global::System.Array.Empty(), global::TUnit.Mocks.TypeArguments.Of.Value); + } + + global::System.Collections.Generic.IEnumerable global::ITestParent.Get() => Get(); + + [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."); + } +} + +internal static class ITestMockFactory +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory(Create); + } + + internal static global::TUnit.Mocks.Mock CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior) + { + var engine = new global::TUnit.Mocks.MockEngine(behavior); + var impl = new ITestMockImpl(engine); + engine.Raisable = impl; + var mock = new ITestMock(impl, engine); + return mock; + } + + internal static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::ITest' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine(behavior); + var impl = new ITestMockImpl(engine); + engine.Raisable = impl; + var mock = new ITestMock(impl, engine); + return mock; + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class ITest_MockMemberExtensions + { + public static global::TUnit.Mocks.MockMethodCall> Get(this global::TUnit.Mocks.Mock mock) where T : notnull + { + var matchers = global::System.Array.Empty(); + return new global::TUnit.Mocks.MockMethodCall>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Get", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); + } + + #if NET9_0_OR_GREATER + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void Reset(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.Reset(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void VerifyAll(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.VerifyAll(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void VerifyNoOtherCalls(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.VerifyNoOtherCalls(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void SetupAllProperties(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.SetupAllProperties(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static global::TUnit.Mocks.Diagnostics.MockDiagnostics GetDiagnostics(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.GetDiagnostics(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void SetState(this global::TUnit.Mocks.Mock mock, string? stateName) + => global::TUnit.Mocks.Mock.SetState(mock, stateName); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void InState(this global::TUnit.Mocks.Mock mock, string stateName, global::System.Action> configure) + => global::TUnit.Mocks.Mock.InState(mock, stateName, configure); + + extension(global::TUnit.Mocks.Mock mock) + { + public global::System.Collections.Generic.IReadOnlyList Invocations => global::TUnit.Mocks.Mock.Invocations(mock); + + public global::TUnit.Mocks.MockBehavior Behavior => global::TUnit.Mocks.Mock.Behavior(mock); + + public global::TUnit.Mocks.IDefaultValueProvider? DefaultValueProvider + { + get => global::TUnit.Mocks.Mock.GetDefaultValueProvider(mock); + set => global::TUnit.Mocks.Mock.SetDefaultValueProvider(mock, value); + } + } + #endif + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks +{ + public static class ITest_MockStaticExtension + { + extension(global::ITest _) + { + public static global::ITestMock Mock() + { + return (global::ITestMock)global::ITestMockFactory.CreateAutoMock(global::TUnit.Mocks.Mock.DefaultBehavior); + } + + public static global::ITestMock Mock(global::TUnit.Mocks.MockBehavior behavior) + { + return (global::ITestMock)global::ITestMockFactory.CreateAutoMock(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/tests/TUnit.Mocks.Tests/Issue6670Tests.cs b/tests/TUnit.Mocks.Tests/Issue6670Tests.cs new file mode 100644 index 0000000000..d6251d3bfb --- /dev/null +++ b/tests/TUnit.Mocks.Tests/Issue6670Tests.cs @@ -0,0 +1,30 @@ +using TUnit.Mocks; + +namespace TUnit.Mocks.Tests; + +public interface IIssue6670Test : IIssue6670TestParent +{ + new IList Get() where T : notnull; +} + +public interface IIssue6670TestParent +{ + IEnumerable Get() where T : notnull; +} + +public class Issue6670Tests +{ + [Test] + public async Task Hidden_Generic_Interface_Method_Mock_Works() + { + var mock = IIssue6670Test.Mock(); + var configured = new List { "configured" }; + mock.Get().Returns(configured); + + IIssue6670Test derived = mock; + IIssue6670TestParent parent = mock; + + await Assert.That(derived.Get()).IsSameReferenceAs(configured); + await Assert.That(parent.Get()).IsSameReferenceAs(configured); + } +}