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
10 changes: 10 additions & 0 deletions docs/docs/execution/test-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ Run all tests with the name `AcceptCookiesTest`:
dotnet run --treenode-filter "/*/*/*/AcceptCookiesTest"
```

### Filter generic tests

Path segments use the names as declared, without type arguments, in both source-generated and reflection mode. A generic class `Repository<T>` is matched by `Repository` (not ``Repository`1``), and a generic method `Method<T>` is matched by `Method`. One filter therefore selects every instantiation:

```bash
dotnet run --treenode-filter "/*/*/Repository/Method"
```

The type arguments appear in the display name instead, written as C# source with keyword aliases and without namespaces: `Method<int>(5)`, `Method<string>(s)`, `Method<List<int>>`.

### Filter by namespace

Run all tests in a specific namespace:
Expand Down
125 changes: 125 additions & 0 deletions src/TUnit.Core.SourceGenerator/Extensions/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,129 @@ public static bool IsNullable(this ITypeSymbol typeSymbol)
}
return null;
}
/// <summary>
/// Formats a generic-method type argument for a test name, such as the <c>int</c> in
/// <c>Method&lt;int&gt;</c>: C# keywords for built-in types, no namespaces, <c>T?</c> for
/// nullable value types, <c>(T1, T2)</c> for value tuples, <c>T[]</c> for arrays and
/// <c>Outer.Inner</c> for nested types.
/// </summary>
/// <remarks>
/// Must stay in step with <c>TypeNameFormatter.GetCSharpTypeName</c> in TUnit.Core, which formats
/// the same names at runtime in reflection mode. Both modes must produce the same name (#7105).
/// </remarks>
public static string ToTestNameTypeArgument(this ITypeSymbol type)
{
var builder = new System.Text.StringBuilder();
AppendTestNameTypeArgument(builder, type);
return builder.ToString();
}

private static void AppendTestNameTypeArgument(System.Text.StringBuilder builder, ITypeSymbol type)
{
switch (type)
{
case ITypeParameterSymbol:
builder.Append(type.Name);
return;
case IDynamicTypeSymbol:
builder.Append("object");
return;
case IArrayTypeSymbol array:
AppendTestNameTypeArgument(builder, array.ElementType);
builder.Append('[');
builder.Append(',', array.Rank - 1);
builder.Append(']');
return;
case IPointerTypeSymbol pointer:
AppendTestNameTypeArgument(builder, pointer.PointedAtType);
builder.Append('*');
return;
}

if (GetTestNameKeyword(type.SpecialType) is { } keyword)
{
builder.Append(keyword);
return;
}

if (type is not INamedTypeSymbol named)
{
builder.Append(type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
return;
}

named = named.TupleUnderlyingType ?? named;

if (named.IsGenericType && named.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
AppendTestNameTypeArgument(builder, named.TypeArguments[0]);
builder.Append('?');
return;
}

if (named is { Name: "ValueTuple", Arity: >= 2 and <= 7, ContainingType: null }
&& named.ContainingNamespace is { Name: "System", ContainingNamespace.IsGlobalNamespace: true })
{
builder.Append('(');
AppendTestNameTypeArguments(builder, named.TypeArguments);
builder.Append(')');
return;
}

AppendTestNameNamedType(builder, named);
}

private static void AppendTestNameNamedType(System.Text.StringBuilder builder, INamedTypeSymbol named)
{
if (named.ContainingType is { } containingType)
{
AppendTestNameNamedType(builder, containingType);
builder.Append('.');
}

builder.Append(named.Name);

if (named.Arity > 0)
{
builder.Append('<');
AppendTestNameTypeArguments(builder, named.TypeArguments);
builder.Append('>');
}
}

private static void AppendTestNameTypeArguments(System.Text.StringBuilder builder, IReadOnlyList<ITypeSymbol> typeArguments)
{
for (var i = 0; i < typeArguments.Count; i++)
{
if (i > 0)
{
builder.Append(", ");
}

AppendTestNameTypeArgument(builder, typeArguments[i]);
}
}

private static string? GetTestNameKeyword(SpecialType specialType) => specialType switch
{
SpecialType.System_Boolean => "bool",
SpecialType.System_Byte => "byte",
SpecialType.System_SByte => "sbyte",
SpecialType.System_Char => "char",
SpecialType.System_Decimal => "decimal",
SpecialType.System_Double => "double",
SpecialType.System_Single => "float",
SpecialType.System_Int16 => "short",
SpecialType.System_UInt16 => "ushort",
SpecialType.System_Int32 => "int",
SpecialType.System_UInt32 => "uint",
SpecialType.System_Int64 => "long",
SpecialType.System_UInt64 => "ulong",
SpecialType.System_String => "string",
SpecialType.System_Object => "object",
SpecialType.System_Void => "void",
SpecialType.System_IntPtr => "nint",
SpecialType.System_UIntPtr => "nuint",
_ => null,
};
}
12 changes: 10 additions & 2 deletions src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
private const string GeneratedNamespace = "TUnit.Generated";

private static readonly DiagnosticDescriptor GenerationErrorDescriptor = new(
"TUNIT0999",

Check warning on line 26 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (tr-TR)

'TUNIT0999' kuralını içeren çözümleyici projesi için çözümleyici yayını izlemeyi etkinleştir (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 26 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (tr-TR)

'TUNIT0999' kuralını içeren çözümleyici projesi için çözümleyici yayını izlemeyi etkinleştir (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 26 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Check warning on line 26 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Check warning on line 26 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (sv-SE)

Check warning on line 26 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (sv-SE)

Check warning on line 26 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Check warning on line 26 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

"Source Generation Error",
"Failed to generate test metadata for {0}.{1}: {2}",
"TUnit",
Expand Down Expand Up @@ -700,7 +700,7 @@
string testName;
if (methodTypeArgs.Length > 0)
{
testName = $"{methodName}<{string.Join(", ", methodTypeArgs.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))}>";
testName = $"{methodName}<{string.Join(", ", methodTypeArgs.Select(t => t.ToTestNameTypeArgument()))}>";
Comment thread
thomhurst marked this conversation as resolved.
Resolved
}
else
{
Expand Down Expand Up @@ -1191,6 +1191,14 @@

writer.AppendLine($"methodName: \"{EscapeString(entry.TestName)}\",");
writer.AppendLine($"fullyQualifiedName: \"{EscapeString(fullyQualifiedName)}\",");

// The test name drops namespaces, so give the test id the fully qualified type arguments
// to keep instantiations over same-named types from different namespaces distinct.
if (entry.MethodTypeArgs.Length > 0)
{
var identity = $"<{string.Join(", ", entry.MethodTypeArgs.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))}>";
writer.AppendLine($"methodTypeArgumentsIdentity: \"{EscapeString(identity)}\",");
}
WriteSourceLocationArguments(writer, testMethod);
WriteFilterDataArguments(writer, testMethod);

Expand Down Expand Up @@ -6092,7 +6100,7 @@
string testName;
if (methodTypeArgs.Length > 0)
{
var methodTypeArgsString = string.Join(", ", methodTypeArgs.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
var methodTypeArgsString = string.Join(", ", methodTypeArgs.Select(t => t.ToTestNameTypeArgument()));
testName = $"{methodName}<{methodTypeArgsString}>";
}
else
Expand Down
3 changes: 2 additions & 1 deletion src/TUnit.Core/DataGeneratorMetadataCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using TUnit.Core.Enums;
using TUnit.Core.Helpers;

namespace TUnit.Core;

Expand Down Expand Up @@ -307,7 +308,7 @@ private static ClassMetadata GetClassMetadataForType(Type type)
{
Type = type,
TypeInfo = new ConcreteType(type),
Name = type.Name,
Name = TypeNameFormatter.GetNameWithoutGenericArity(type),
Namespace = type.Namespace ?? string.Empty,
Assembly = AssemblyMetadata.GetOrAdd(type.Assembly.GetName().Name ?? type.Assembly.GetName().FullName ?? "Unknown", () => new AssemblyMetadata
{
Expand Down
Loading
Loading