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
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,32 @@ public static GenericConstraintViolationDataSource Create(string message, IDataS
public IDataSourceAttribute? InnerDataSource { get; }

/// <inheritdoc />
/// <remarks>Mirrors the inner data source, so an empty source behaves as it would without the wrapper.</remarks>
public bool SkipIfEmpty { get => InnerDataSource?.SkipIfEmpty ?? false; set { } }
/// <remarks>Forwards to the inner data source, so an empty source behaves as it would without the wrapper.</remarks>
public bool SkipIfEmpty
{
get => InnerDataSource?.SkipIfEmpty ?? false;
set
{
if (InnerDataSource is not null)
{
InnerDataSource.SkipIfEmpty = value;
}
}
}

/// <inheritdoc />
/// <remarks>Mirrors the inner data source.</remarks>
public bool DeferEnumeration { get => InnerDataSource?.DeferEnumeration ?? false; set { } }
/// <remarks>Forwards to the inner data source.</remarks>
public bool DeferEnumeration
{
get => InnerDataSource?.DeferEnumeration ?? false;
set
{
if (InnerDataSource is not null)
{
InnerDataSource.DeferEnumeration = value;
}
}
}

/// <inheritdoc />
public async IAsyncEnumerable<Func<Task<object?[]?>>> GetDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
Expand Down
97 changes: 83 additions & 14 deletions src/TUnit.Engine/Services/TestGenericTypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public static TestGenericTypeResolution Resolve(TestMetadata metadata, TestBuild
metadata.MethodMetadata,
metadata.GenericMethodInfo,
testData.MethodData,
parameterTypes);
parameterTypes,
result.ResolvedClassGenericArguments);
}

return result;
Expand Down Expand Up @@ -119,7 +120,7 @@ private static Type[] ResolveClassGenericArguments(
}

// Validate against constraints from metadata
ValidateAgainstConstraints(genericTypeInfo.Constraints, resolvedTypes);
ValidateAgainstConstraints(genericTypeInfo.Constraints, resolvedTypes, classTypeArguments: resolvedTypes);

return resolvedTypes;
}
Expand All @@ -131,7 +132,8 @@ private static Type[] ResolveMethodGenericArguments(
MethodMetadata methodMetadata,
GenericMethodInfo genericMethodInfo,
object?[] methodArguments,
Type[] parameterTypes)
Type[] parameterTypes,
Type[] classTypeArguments)
{
// Handle the case where some parameter types are Object (placeholders for generic parameters)
// This happens when data sources provide untyped data or when we have mixed generic/non-generic parameters
Expand Down Expand Up @@ -161,7 +163,7 @@ private static Type[] ResolveMethodGenericArguments(
resolvedTypes[0] = methodArguments[i]!.GetType();

// Validate against constraints from metadata
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypes);
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypes, classTypeArguments);

return resolvedTypes;
}
Expand Down Expand Up @@ -199,7 +201,7 @@ private static Type[] ResolveMethodGenericArguments(
}

// Validate against constraints from metadata
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypes);
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypes, classTypeArguments);

return resolvedTypes;
}
Expand Down Expand Up @@ -365,7 +367,7 @@ private static Type[] ResolveMethodGenericArguments(
}

// Validate against constraints from metadata
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypes);
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypes, classTypeArguments);

return resolvedTypes;
}
Expand All @@ -386,7 +388,7 @@ private static Type[] ResolveMethodGenericArguments(
}

// Validate against constraints from metadata
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypes);
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypes, classTypeArguments);

return resolvedTypes;
}
Expand Down Expand Up @@ -445,7 +447,7 @@ private static Type[] ResolveMethodGenericArguments(
}

// Validate against constraints from metadata
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypesFromMapping);
ValidateAgainstConstraints(genericMethodInfo.Constraints, resolvedTypesFromMapping, classTypeArguments);

return resolvedTypesFromMapping;
}
Expand Down Expand Up @@ -596,9 +598,16 @@ private static bool TryInferTypeMapping(
return parameterType.IsAssignableFrom(argumentType);
}

/// <param name="constraints">The constraints of the generic parameters that <paramref name="resolvedTypes"/> fill.</param>
/// <param name="resolvedTypes">The resolved type arguments, one per constraint.</param>
/// <param name="classTypeArguments">
/// The resolved type arguments of the test class, substituted for class type parameters that a constraint
/// mentions (e.g. <c>where TMethod : IComparable&lt;TClass&gt;</c>).
/// </param>
private static void ValidateAgainstConstraints(
GenericParameterConstraints[] constraints,
Type[] resolvedTypes)
Type[] resolvedTypes,
Type[] classTypeArguments)
{
for (var i = 0; i < Math.Min(constraints.Length, resolvedTypes.Length); i++)
{
Expand Down Expand Up @@ -630,16 +639,20 @@ private static void ValidateAgainstConstraints(
}

// Check base type constraint
if (constraint.BaseTypeConstraint != null &&
!constraint.BaseTypeConstraint.IsAssignableFrom(resolvedType))
if (constraint.BaseTypeConstraint != null)
{
throw new GenericTypeResolutionException(
$"Type '{resolvedType.Name}' does not satisfy the base type constraint '{constraint.BaseTypeConstraint.Name}' for generic parameter '{constraint.ParameterName}'");
var baseTypeConstraint = SubstituteGenericParameters(constraint.BaseTypeConstraint, classTypeArguments, resolvedTypes);
if (!baseTypeConstraint.IsAssignableFrom(resolvedType))
{
throw new GenericTypeResolutionException(
$"Type '{resolvedType.Name}' does not satisfy the base type constraint '{baseTypeConstraint.Name}' for generic parameter '{constraint.ParameterName}'");
}
}

// Check interface constraints
foreach (var interfaceConstraint in constraint.InterfaceConstraints)
foreach (var constraintType in constraint.InterfaceConstraints)
{
var interfaceConstraint = SubstituteGenericParameters(constraintType, classTypeArguments, resolvedTypes);
if (!interfaceConstraint.IsAssignableFrom(resolvedType))
{
throw new GenericTypeResolutionException(
Expand All @@ -648,6 +661,62 @@ private static void ValidateAgainstConstraints(
}
}
}

/// <summary>
/// Replaces the generic parameters in a constraint type (e.g. <c>IComparable&lt;T&gt;</c> or
/// <c>IEnumerable&lt;U[]&gt;</c>) with their resolved type arguments, so the constraint can be checked
/// with <see cref="Type.IsAssignableFrom"/>. Class type parameters come from <paramref name="classTypeArguments"/>
/// and method type parameters from <paramref name="methodTypeArguments"/>. A parameter with no resolved
/// argument leaves the constraint unchanged.
/// </summary>
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Only constraint types that mention generic parameters are rebuilt. Those come from open generic tests, which are resolved at runtime in reflection mode only; source-generated tests are already closed.")]
[UnconditionalSuppressMessage("Trimming", "IL2055", Justification = "The generic definition and its arguments come from the constraint and the resolved test types, which are already in use.")]
private static Type SubstituteGenericParameters(Type type, Type[] classTypeArguments, Type[] methodTypeArguments)
{
if (!type.ContainsGenericParameters)
{
return type;
}

if (type.IsGenericParameter)
{
var typeArguments = type.DeclaringMethod != null ? methodTypeArguments : classTypeArguments;
var position = type.GenericParameterPosition;
return position < typeArguments.Length ? typeArguments[position] : type;
}

if (type.IsArray)
{
var elementType = type.GetElementType()!;
var substitutedElement = SubstituteGenericParameters(elementType, classTypeArguments, methodTypeArguments);
if (substitutedElement.ContainsGenericParameters)
{
return type;
}

// A single-dimensional zero-based array (T[]) differs from a rank-1 multi-dimensional one (T[*])
return type == elementType.MakeArrayType()
? substitutedElement.MakeArrayType()
: substitutedElement.MakeArrayType(type.GetArrayRank());
}

if (type.IsGenericType)
{
var typeArguments = type.GetGenericArguments();
for (var i = 0; i < typeArguments.Length; i++)
{
typeArguments[i] = SubstituteGenericParameters(typeArguments[i], classTypeArguments, methodTypeArguments);
if (typeArguments[i].ContainsGenericParameters)
{
return type;
}
}

return type.GetGenericTypeDefinition().MakeGenericType(typeArguments);
}

return type;
}
}

/// <summary>
Expand Down
Loading
Loading