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
6 changes: 6 additions & 0 deletions eng/Roslyn.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<AdditionalFiles Include="..\$(_BaseProjectName)\AnalyzerReleases.Unshipped.md" />
</ItemGroup>

<!-- Core source generator: the [GenerateGenericTest] arity messages are shared with TUnit.Engine (#7222),
as the base project's TUnit.Core.SourceGenerator.csproj links them -->
<ItemGroup Condition="'$(_BaseProjectName)' == 'TUnit.Core.SourceGenerator'">
<Compile Include="..\TUnit.Core\Helpers\GenerateGenericTestArityMessages.cs" Link="Shared\GenerateGenericTestArityMessages.cs" />
</ItemGroup>

<!-- SourceGenerator: include folder link for IDE navigation -->
<ItemGroup Condition="$(_BaseProjectName.Contains('SourceGenerator'))">
<Folder Include="..\$(_BaseProjectName)\CodeGenerators\Writers\Hooks\">
Expand Down
356 changes: 303 additions & 53 deletions src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
<Compile Remove="Extensions\CompilationExtensions.cs" />
<Compile Remove="Helpers\GenericTypeInference.cs" />
</ItemGroup>
<ItemGroup>
<!-- Shared with TUnit.Engine so both modes report the same [GenerateGenericTest] arity message (#7222).
The Roslyn44/47/414 variant projects do not compile this csproj's items, so eng/Roslyn.props adds the same
link for them; no project imports both, so the file is compiled once per project. -->
<Compile Include="..\TUnit.Core\Helpers\GenerateGenericTestArityMessages.cs" Link="Shared\GenerateGenericTestArityMessages.cs" />
Comment thread
thomhurst marked this conversation as resolved.
Resolved
</ItemGroup>
<ItemGroup>
<Folder Include="CodeGenerators\Writers\Hooks\" />
</ItemGroup>
Expand Down
5 changes: 3 additions & 2 deletions src/TUnit.Core/GenericConstraintViolationEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public static GenericConstraintViolationTestMetadata Create(
MethodMetadata methodMetadata,
string filePath,
int lineNumber,
IDataSourceAttribute[] dataSources)
IDataSourceAttribute[] dataSources,
Func<Attribute[]>? attributeFactory = null)
{
return new GenericConstraintViolationTestMetadata
{
Expand All @@ -94,7 +95,7 @@ public static GenericConstraintViolationTestMetadata Create(
FilePath = filePath,
LineNumber = lineNumber,
MethodMetadata = methodMetadata,
AttributeFactory = static () => [],
AttributeFactory = attributeFactory ?? (static () => []),
DataSources = dataSources,
ClassDataSources = [],
PropertyDataSources = [],
Expand Down
26 changes: 26 additions & 0 deletions src/TUnit.Core/Helpers/GenerateGenericTestArityMessages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Globalization;

namespace TUnit.Core.Helpers;

/// <summary>
/// The failure messages for a method-level <c>[GenerateGenericTest]</c> whose type arguments cannot close the
/// method. Reflection mode (TUnit.Engine) uses this class, and TUnit.Core.SourceGenerator compiles this same file,
/// so both modes report the same text (#7222). Callers pass type names already formatted as <c>Type.Name</c> gives them.
/// </summary>
internal static class GenerateGenericTestArityMessages
{
/// <summary>How a null type argument, such as <c>[GenerateGenericTest((Type)null!)]</c>, is named.</summary>
public const string NullTypeName = "null";

public static string NoTypeArguments(string methodName)
=> $"[GenerateGenericTest] on method '{methodName}' has no type arguments";

public static string WrongCount(string methodName, int providedCount, int requiredCount, IEnumerable<string> providedTypeNames)
=> $"[GenerateGenericTest] on method '{methodName}' provides {providedCount.ToString(CultureInfo.InvariantCulture)} type argument(s) "
+ $"but method requires {requiredCount.ToString(CultureInfo.InvariantCulture)}. "
+ $"Provided: [{string.Join(", ", providedTypeNames)}]";

public static string InvalidTypeArgument(string methodName, string typeName)
=> $"[GenerateGenericTest] on method '{methodName}' provides type argument '{typeName}', which cannot be a generic type argument";
}
18 changes: 16 additions & 2 deletions src/TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1632,7 +1632,8 @@ internal static Exception ToReportedException(Exception exception)

private TestDetails CreateFailedTestDetails(TestMetadata metadata, string testId, object?[] failedDataRowObjects)
{
return new TestDetails([])
var attributes = GetFailedTestAttributes(metadata);
return new TestDetails(attributes)
{
TestId = testId,
UnreceivedDataRowObjects = failedDataRowObjects,
Expand All @@ -1649,10 +1650,23 @@ private TestDetails CreateFailedTestDetails(TestMetadata metadata, string testId
TestEndColumnNumber = metadata.EndColumnNumber,
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
AttributesByType = AttributeDictionaryHelper.Empty
AttributesByType = attributes.Length == 0 ? AttributeDictionaryHelper.Empty : new LazyAttributeDictionary(attributes)
};
}

internal static Attribute[] GetFailedTestAttributes(TestMetadata metadata)
{
try
{
return ScopeRowAttributesToTest(metadata.GetOrCreateAttributes(), null, null);
}
catch
{
// An attribute factory can itself be the discovery failure. Keep reporting that failure.
return [];
}
}

/// <summary>
/// Gives a test the data sources that produced it. A data source that is also an
/// <see cref="ITestRegisteredEventReceiver"/>, such as <see cref="ArgumentsAttribute"/>, carries
Expand Down
5 changes: 3 additions & 2 deletions src/TUnit.Engine/Building/TestBuilderPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada
var testId = TestIdentifierService.GenerateFailedTestId(metadata);
var displayName = $"{metadata.TestClassType.Name}.{metadata.TestName}";

var testDetails = new TestDetails([])
var attributes = TestBuilder.GetFailedTestAttributes(metadata);
var testDetails = new TestDetails(attributes)
{
TestId = testId,
TestName = metadata.TestName,
Expand All @@ -286,7 +287,7 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada
TestEndColumnNumber = metadata.EndColumnNumber,
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
AttributesByType = AttributeDictionaryHelper.Empty
AttributesByType = attributes.Length == 0 ? AttributeDictionaryHelper.Empty : new LazyAttributeDictionary(attributes)
};

var context = _contextProvider.CreateTestContext(
Expand Down
70 changes: 49 additions & 21 deletions src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1973,26 +1973,11 @@ private static bool HasTestMethods([DynamicallyAccessedMembers(DynamicallyAccess
{
var typeArguments = genAttr.TypeArguments;

// Validate type argument count
if (typeArguments.Length == 0)
// A wrongly sized or empty [GenerateGenericTest] fails like a constraint violation, in declaration
// order and with the same message as source-generated mode, so both modes report the same test (#7222).
if (GetGenerateGenericTestArityError(method.Name, typeArguments, methodGenericParams.Length) is { } arityError)
{
var failedMetadata = CreateFailedMethodGenericMetadata(
concreteClassType,
method,
$"[GenerateGenericTest] on method '{method.Name}' has no type arguments");
yield return (method, failedMetadata, null);
continue;
}

if (typeArguments.Length != methodGenericParams.Length)
{
var failedMetadata = CreateFailedMethodGenericMetadata(
concreteClassType,
method,
$"[GenerateGenericTest] on method '{method.Name}' provides {typeArguments.Length} type argument(s) " +
$"but method requires {methodGenericParams.Length}. " +
$"Provided: [{string.Join(", ", typeArguments.Select(static t => t.Name))}]");
yield return (method, failedMetadata, null);
(constraintViolations ??= []).Add(GenericConstraintViolationDataSource.Create(arityError));
Comment thread
thomhurst marked this conversation as resolved.
Resolved
continue;
}

Expand Down Expand Up @@ -2054,7 +2039,8 @@ private static bool HasTestMethods([DynamicallyAccessedMembers(DynamicallyAccess
ReflectionMetadataBuilder.CreateMethodMetadata(concreteClassType, method),
testAttribute?.File ?? "Unknown",
testAttribute?.Line ?? 0,
constraintViolations.ToArray()), null);
constraintViolations.ToArray(),
() => GetFailedTestAttributes(method)), null);
Comment thread
thomhurst marked this conversation as resolved.
Resolved
}
}

Expand Down Expand Up @@ -2166,6 +2152,46 @@ private static IDataSourceAttribute[] ExcludeDeclaredTypedSources(MethodInfo met
}
}

/// <summary>
/// The failure message for a method-level [GenerateGenericTest] whose type arguments do not match the
/// method's type parameters or that names a null or pointer type, or null when it is valid. Source-generated mode
/// (<c>DescribeGenerateGenericTestArityError</c> in TUnit.Core.SourceGenerator) builds the text from the same
/// <see cref="GenerateGenericTestArityMessages"/>.
/// </summary>
private static string? GetGenerateGenericTestArityError(string methodName, Type[] typeArguments, int methodTypeParameterCount)
{
if (typeArguments.Length == 0)
{
return GenerateGenericTestArityMessages.NoTypeArguments(methodName);
}

if (typeArguments.Length != methodTypeParameterCount)
{
return GenerateGenericTestArityMessages.WrongCount(
methodName, typeArguments.Length, methodTypeParameterCount, typeArguments.Select(static t => t?.Name ?? GenerateGenericTestArityMessages.NullTypeName));
}

foreach (var typeArgument in typeArguments)
{
// [GenerateGenericTest((Type)null!)] stores a null element
if (typeArgument is null)
{
return GenerateGenericTestArityMessages.InvalidTypeArgument(methodName, GenerateGenericTestArityMessages.NullTypeName);
}

if (typeArgument.IsPointer
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
#if NET8_0_OR_GREATER
|| typeArgument.IsFunctionPointer
#endif
)
{
return GenerateGenericTestArityMessages.InvalidTypeArgument(methodName, typeArgument.Name);
}
}

return null;
}

/// <summary>
/// Closes <paramref name="method"/> over the types inferred from its data sources, in the order the source
/// generator uses:
Expand Down Expand Up @@ -3134,7 +3160,9 @@ private static TestMetadata CreateFailedMethodGenericMetadata(
/// Otherwise every row's Skip, DisplayName and Categories would be applied to the failed test (#7076).
/// </summary>
private static Attribute[] GetFailedTestAttributes(MethodInfo method)
=> TestBuilder.ScopeRowAttributesToTest(method.GetCustomAttributes().ToArray(), null, null);
=> TestBuilder.ScopeRowAttributesToTest(
ReflectionAttributeExtractor.GetAllAttributes(method.ReflectedType ?? method.DeclaringType!, method),
null, null);

private static string? ExtractFilePath(MethodInfo method)
{
Expand Down
9 changes: 7 additions & 2 deletions tests/TUnit.Core.SourceGenerator.Tests/Bugs/7116/Tests7116.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,16 @@ await Assert.That(SourceFor("TypedObjectDeclaredType"))
await Assert.That(exactAndObject).Contains("DeclaredTypeInferenceTests.ExactStreams()");
await Assert.That(exactAndObject).Contains("DeclaredTypeInferenceTests.Streams()");

// An empty or wrongly sized [GenerateGenericTest] does not suppress the violation, so the rows are not lost.
// An empty or wrongly sized [GenerateGenericTest] fails on its own, so the rows are not silently lost.
// As in reflection mode, the object-declared rows are not reported as well (#7222).
await Assert.That(SourceFor("EmptyGenerateGenericTestWithObjectRows"))
.Contains(violation + "\"[GenerateGenericTest] on method 'EmptyGenerateGenericTestWithObjectRows' has no type arguments\")");
await Assert.That(SourceFor("WrongArityGenerateGenericTestWithObjectRows"))
.Contains(violation + "\"[GenerateGenericTest] on method 'WrongArityGenerateGenericTestWithObjectRows' provides 2 type argument(s) but method requires 1. Provided: [MemoryStream, Int32]\")");
foreach (var methodName in new[] { "EmptyGenerateGenericTestWithObjectRows", "WrongArityGenerateGenericTestWithObjectRows" })
{
await Assert.That(SourceFor(methodName))
.Contains("'T' was inferred from the declared type 'object' of the data source");
.DoesNotContain("'T' was inferred from the declared type 'object' of the data source");
}
});
}
Loading
Loading