Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,35 @@ void M()
return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_Generic_Method_Constraints_On_Explicit_Impl()
{
var source = """
using System;
using TUnit.Mocks;

public interface IConstrained
{
T GetNotnull<T>(string key) where T : notnull;
T GetNew<T>() where T : new();
T GetUnmanaged<T>() where T : unmanaged;
T GetDisposable<T>() where T : IDisposable;
T GetClassNew<T>() where T : class, IDisposable, new();
T GetStructDisposable<T>() where T : struct, IDisposable;
}

public class TestUsage
{
void M()
{
var mock = Mock.Of<IConstrained>();
}
}
""";

return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_Overloaded_Methods()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
public sealed class IConstrainedMock : global::TUnit.Mocks.Mock<global::IConstrained>, global::IConstrained
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal IConstrainedMock(global::IConstrained mockObject, global::TUnit.Mocks.MockEngine<global::IConstrained> engine)
: base(mockObject, engine) { }

T global::IConstrained.GetNotnull<T>(string key) => Object.GetNotnull<T>(key);

T global::IConstrained.GetNew<T>() => Object.GetNew<T>();

T global::IConstrained.GetUnmanaged<T>() where T : struct => Object.GetUnmanaged<T>();

T global::IConstrained.GetDisposable<T>() => Object.GetDisposable<T>();

T global::IConstrained.GetClassNew<T>() where T : class => Object.GetClassNew<T>();

T global::IConstrained.GetStructDisposable<T>() where T : struct => Object.GetStructDisposable<T>();
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
file sealed class IConstrainedMockImpl : global::IConstrained, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
{
private readonly global::TUnit.Mocks.MockEngine<global::IConstrained> _engine;

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; }

internal IConstrainedMockImpl(global::TUnit.Mocks.MockEngine<global::IConstrained> engine)
{
_engine = engine;
}

public T GetNotnull<T>(string key) where T : notnull
{
return _engine.HandleCallWithReturn<T, string>(0, "GetNotnull", key, default!);
}

public T GetNew<T>() where T : new()
{
return _engine.HandleCallWithReturn<T>(1, "GetNew", global::System.Array.Empty<object?>(), default!);
}

public T GetUnmanaged<T>() where T : struct, unmanaged
{
return _engine.HandleCallWithReturn<T>(2, "GetUnmanaged", global::System.Array.Empty<object?>(), default);
}

public T GetDisposable<T>() where T : global::System.IDisposable
{
return _engine.HandleCallWithReturn<T>(3, "GetDisposable", global::System.Array.Empty<object?>(), default!);
}

public T GetClassNew<T>() where T : class, global::System.IDisposable, new()
{
return _engine.HandleCallWithReturn<T>(4, "GetClassNew", global::System.Array.Empty<object?>(), default!);
}

public T GetStructDisposable<T>() where T : struct, global::System.IDisposable
{
return _engine.HandleCallWithReturn<T>(5, "GetStructDisposable", global::System.Array.Empty<object?>(), default);
}

[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 IConstrainedMockFactory
{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register()
{
global::TUnit.Mocks.MockRegistry.RegisterFactory<global::IConstrained>(Create);
}

internal static global::TUnit.Mocks.Mock<global::IConstrained> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::IConstrained' does not support constructor arguments, but {constructorArgs.Length} were provided.");
var engine = new global::TUnit.Mocks.MockEngine<global::IConstrained>(behavior);
var impl = new IConstrainedMockImpl(engine);
engine.Raisable = impl;
var mock = new IConstrainedMock(impl, engine);
return mock;
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
public static class IConstrained_MockMemberExtensions
{
public static global::TUnit.Mocks.MockMethodCall<T> GetNotnull<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock, global::TUnit.Mocks.Arguments.Arg<string> key) where T : notnull
{
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { key.Matcher };
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetNotnull", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetNotnull<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock, global::System.Func<string, bool> key) where T : notnull
{
global::TUnit.Mocks.Arguments.Arg<string> __fa_key = key;
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_key.Matcher };
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetNotnull", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetNew<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : new()
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetNew", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetUnmanaged<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : struct, unmanaged
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "GetUnmanaged", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetDisposable<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : global::System.IDisposable
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "GetDisposable", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetClassNew<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : class, global::System.IDisposable, new()
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 4, "GetClassNew", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetStructDisposable<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : struct, global::System.IDisposable
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 5, "GetStructDisposable", matchers);
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks
{
public static class IConstrained_MockStaticExtension
{
extension(global::IConstrained)
{
public static global::TUnit.Mocks.Generated.IConstrainedMock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose)
{
return (global::TUnit.Mocks.Generated.IConstrainedMock)global::TUnit.Mocks.Mock.Of<global::IConstrained>(behavior);
}
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated;
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ namespace TUnit.Mocks.Generated

T global::IRepository.GetById<T>(int id) where T : class => Object.GetById<T>(id);

void global::IRepository.Save<T>(T entity) where T : class, new()
void global::IRepository.Save<T>(T entity) where T : class
{
Object.Save<T>(entity);
}

TResult global::IRepository.Transform<TInput, TResult>(TInput input) where TInput : notnull where TResult : struct => Object.Transform<TInput, TResult>(input);
TResult global::IRepository.Transform<TInput, TResult>(TInput input) where TResult : struct => Object.Transform<TInput, TResult>(input);
}
}

Expand Down
20 changes: 16 additions & 4 deletions TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,13 +1112,25 @@ private static string FormatConstraintClauses(EquatableArray<MockTypeParameterMo
var clauses = new List<string>();
foreach (var tp in typeParameters)
{
if (!string.IsNullOrEmpty(tp.Constraints))
if (forExplicitImplementation)
{
clauses.Add($"where {tp.Name} : {tp.Constraints}");
// CS0460: Only 'class' and 'struct' constraints are allowed on explicit interface implementations.
if (tp.HasReferenceTypeConstraint)
{
clauses.Add($"where {tp.Name} : class");
}
else if (tp.HasValueTypeConstraint)
{
clauses.Add($"where {tp.Name} : struct");
}
else if (tp.HasAnnotatedNullableUsage)
{
clauses.Add($"where {tp.Name} : default");
}
}
else if (forExplicitImplementation && tp.HasAnnotatedNullableUsage)
else if (!string.IsNullOrEmpty(tp.Constraints))
{
clauses.Add($"where {tp.Name} : default");
clauses.Add($"where {tp.Name} : {tp.Constraints}");
}
}
return clauses.Count > 0 ? " " + string.Join(' ', clauses) : "";
Expand Down
2 changes: 2 additions & 0 deletions TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m
{
Name = tp.Name,
Constraints = tp.GetGenericConstraints(),
HasReferenceTypeConstraint = tp.HasReferenceTypeConstraint,
HasValueTypeConstraint = tp.HasValueTypeConstraint,
HasAnnotatedNullableUsage = tp.IsUnconstrainedWithNullableUsage(method)
}).ToImmutableArray()
),
Expand Down
10 changes: 9 additions & 1 deletion TUnit.Mocks.SourceGenerator/Models/MockTypeParameterModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ internal sealed record MockTypeParameterModel : IEquatable<MockTypeParameterMode
{
public string Name { get; init; } = "";
public string Constraints { get; init; } = "";
public bool HasReferenceTypeConstraint { get; init; }
public bool HasValueTypeConstraint { get; init; }
public bool HasAnnotatedNullableUsage { get; init; }

public bool Equals(MockTypeParameterModel? other)
{
if (other is null) return false;
return Name == other.Name && Constraints == other.Constraints && HasAnnotatedNullableUsage == other.HasAnnotatedNullableUsage;
return Name == other.Name
&& Constraints == other.Constraints
&& HasReferenceTypeConstraint == other.HasReferenceTypeConstraint
&& HasValueTypeConstraint == other.HasValueTypeConstraint
&& HasAnnotatedNullableUsage == other.HasAnnotatedNullableUsage;
}

public override int GetHashCode()
Expand All @@ -21,6 +27,8 @@ public override int GetHashCode()
int hash = 17;
hash = hash * 31 + Name.GetHashCode();
hash = hash * 31 + Constraints.GetHashCode();
hash = hash * 31 + HasReferenceTypeConstraint.GetHashCode();
hash = hash * 31 + HasValueTypeConstraint.GetHashCode();
hash = hash * 31 + HasAnnotatedNullableUsage.GetHashCode();
return hash;
}
Expand Down
81 changes: 81 additions & 0 deletions TUnit.Mocks.Tests/GenericConstraintTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using TUnit.Mocks;

namespace TUnit.Mocks.Tests;

/// <summary>
/// Interfaces with various generic method constraints that previously caused CS0460
/// when the source generator copied non-class/struct constraints to explicit interface implementations.
/// </summary>
public interface INotnullService
{
T Get<T>(string key) where T : notnull;
}

public interface INewConstraintService
{
T Create<T>() where T : new();
}

public interface IBaseTypeConstraintService
{
T Get<T>() where T : IDisposable;
}

public interface ICompositeConstraintService
{
T Create<T>() where T : class, IDisposable, new();
T Read<T>() where T : struct, IComparable<T>;
}

public class GenericConstraintTests
{
[Test]
public async Task Notnull_Constraint_Mock_Returns_Configured_Value()
{
var mock = Mock.Of<INotnullService>();
mock.Get<int>("key").Returns(42);

INotnullService svc = mock.Object;
var result = svc.Get<int>("key");

await Assert.That(result).IsEqualTo(42);
}

[Test]
public void New_Constraint_Mock_Does_Not_Throw()
{
var mock = Mock.Of<INewConstraintService>();

INewConstraintService svc = mock.Object;
_ = svc.Create<object>();
}

[Test]
public void Base_Type_Constraint_Mock_Does_Not_Throw()
{
var mock = Mock.Of<IBaseTypeConstraintService>();

IBaseTypeConstraintService svc = mock.Object;
_ = svc.Get<MemoryStream>();
}

[Test]
public void Composite_Class_Constraint_Mock_Does_Not_Throw()
{
var mock = Mock.Of<ICompositeConstraintService>();

ICompositeConstraintService svc = mock.Object;
_ = svc.Create<MemoryStream>();
}

[Test]
public async Task Composite_Struct_Constraint_Mock_Returns_Default()
{
var mock = Mock.Of<ICompositeConstraintService>();

ICompositeConstraintService svc = mock.Object;
var result = svc.Read<int>();

await Assert.That(result).IsEqualTo(0);
}
}
Loading