Skip to content

Generic test tree-node paths and display names differ between source-gen and reflection modes #7105

Closed

Description

@thomhurst

Summary

Generic tests get a different tree-node path and display name in source-gen mode than in reflection mode. So an exact --treenode-filter for a generic test only matches in one mode, and test names in reports and IDEs change when the engine mode changes.

Confirmed on current main (b1cceb3), net10.0.

Repro

namespace TUnit.TestProject.FollowupRepro7103;

public class FxNamesMethod
{
    [Test]
    [Arguments(5)]
    [Arguments("s")]
    public async Task Method<T>(T value) => await Assert.That(value is not null).IsTrue();
}

[Arguments(5)]
public class FxNamesClass<T>(T value)
{
    [Test]
    public async Task Plain() => await Assert.That(value is not null).IsTrue();
}

#pragma warning disable TUnit0001 // false positive, see the TUnit0001 issue
public class FxAnalyzer
{
    public static IEnumerable<Func<(int, string)>> Rows() { yield return () => (1, "a"); }

    [Test]
    [GenerateGenericTest(typeof(long), typeof(string))]
    [MethodDataSource(nameof(Rows))]
    public async Task A_GenerateGeneric_Tuple<T1, T2>(T1 first, T2 second) => await Assert.That(first is T1).IsTrue();
}

Tree-node path (what --treenode-filter matches)

Here N stands for /*/TUnit.TestProject.FollowupRepro7103. Cells are the number of tests that ran.

Filter Source-gen Reflection
N/FxNamesMethod/Method 0 2
N/FxNamesMethod/Method<int> 1 0
N/FxNamesMethod/Method<Int32> 0 0
N/FxNamesMethod/Method* 2 2
N/FxNamesClass/Plain 1 0
N/FxNamesClass`1/Plain 0 1
N/FxNamesClass<int>/Plain 0 0
N/FxNamesClass*/Plain 1 1
N/FxAnalyzer/A_GenerateGeneric_Tuple 0 1
N/FxAnalyzer/A_GenerateGeneric_Tuple<long, string> 1 0
N/FxAnalyzer/A_GenerateGeneric_Tuple<Int64, String> 0 0

So:

Display name

Test Source-gen Reflection
Method, row 5 Method<int>(5) Method(5)
Method, row "s" Method<string>(s) Method(s)
A_GenerateGeneric_Tuple A_GenerateGeneric_Tuple<long, string>(1, a) A_GenerateGeneric_Tuple<Int64, String>(1, a)
B_Tuple_Only<T1, T2> (same data source, no [GenerateGenericTest]) B_Tuple_Only<int, string>(1, a) B_Tuple_Only(1, a)
[GenerateGenericTest(typeof(int))] GenerateGeneric<T>() GenerateGeneric<int> GenerateGeneric<Int32>
FxNamesClass<T>.Plain Plain Plain

Reflection shows type arguments only when it closed the method with MakeGenericMethod, which happens on the [GenerateGenericTest] and typed-data-source paths. Even then it uses CLR names (Int32, Int64, String). When T is resolved per row while the test is built, reflection shows no type arguments at all. Source-gen always shows type arguments, with C# keyword names.

Expected

The same tree-node path and display name in both modes, so that one filter selects the same tests in either mode. Which form to keep is an owner decision. One option is a method node of Method<int> and a class node of either FxNamesClass or FxNamesClass`1 in both modes. Either way, the type arguments must be formatted the same way in both modes: keyword or CLR name, and with or without global::.

Suspected cause / file pointers

Source-gen:

  • src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs, CollectConcreteInstantiations → TryAddInstantiation (around line 700): testName = $"{methodName}<{...ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>". That TestName becomes the entry's method name and TestMethodName (emitted around lines 1172-1192).
  • src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs, lines 91 and 181: ClassMetadata.Name = typeSymbol.Name, which has no arity.

Reflection:

  • src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs, around line 1091: TestMethodName = testMethod.Name, the bare name. The display-name builder around lines 1143-1153 adds <{t.Name}> (CLR names), and only for closed generic methods.
  • Same file, around line 1461: ClassMetadata.Name = type.Name, which gives FxNamesClass`1.

The filter path is built from these values in src/TUnit.Engine/Services/TestFilterService.cs BuildPath (line 219) and in src/TUnit.Engine/Services/MetadataFilterMatcher.cs BuildPathFromMetadata.

Because of this mismatch, tests/TUnit.Engine.Tests/GenericConstraintValidationTests.cs in #7102 had to put a trailing * on its method filters.

Related: #7094, #7100, #7102

Activity

  1. added a commit that references this issue on Oct 18, 2026
    faf43b6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions