Skip to content

Generic test classes drop [Arguments] rows that infer the same type arguments (both modes; reflection runs no method rows) #7106

Closed

Description

@thomhurst

Summary

#7100 fixes generic methods in non-generic classes, where only the first [Arguments] row per inferred type ran in source-gen mode. Generic classes still drop rows, in both modes and in different ways:

  • Several class-level [Arguments] rows that infer the same T run only the first row, in both modes.
  • Method-level [Arguments] rows on a non-generic method of a generic class (where the rows infer the class's T) run only the first row per type in source-gen mode, and no rows at all in reflection mode.
  • Class rows multiplied by generic-method rows lose every class row after the first one per type, in both modes.
  • A generic class with a class-level row plus a non-generic method with its own [Arguments] row fails with IndexOutOfRangeException in source-gen mode, but passes in reflection mode.

Nothing is reported for the dropped rows. They are just never discovered. This is the follow-up noted in #7096 ("Related observations").

Confirmed on current main (b1cceb3), net10.0. #7100 is not merged yet, but it does not touch generic classes.

Repro

namespace TUnit.TestProject.FollowupRepro7103;

[Arguments(1)]
[Arguments(2)]
[Arguments(3)]
public class FxClassRowsSame<T>(T value)                    // expected 3 tests
{
    [Test]
    public async Task Run() => await Assert.That(value).IsTypeOf<int>();
}

[Arguments(1)]
[Arguments("a")]
[Arguments(2)]
[Arguments("b")]
public class FxClassRowsMixed<T>(T value)                   // expected 4 tests
{
    [Test]
    public async Task Run() => await Assert.That(value is int or string).IsTrue();
}

public class FxMethodRowsOnGenericClass<T>                  // expected 2 tests
{
    [Test]
    [Arguments(1)]
    [Arguments(2)]
    public async Task M(T value) => await Assert.That(value).IsTypeOf<int>();
}

public class FxMixedMethodRowsOnGenericClass<T>             // expected 3 tests
{
    [Test]
    [Arguments(1)]
    [Arguments("a")]
    [Arguments(2)]
    public async Task M(T value) => await Assert.That(value is int or string).IsTrue();
}

[Arguments(1)]
[Arguments(2)]
public class FxClassAndMethodRows<T>(T value)               // expected 2 x 2 = 4 tests
{
    [Test]
    [Arguments("x")]
    [Arguments("y")]
    public async Task M<T2>(T2 other) => await Assert.That(value).IsTypeOf<int>();
}

[Arguments(5)]
public class FxNamesClass<T>(T value)                       // expected WithArg(7) to pass
{
    [Test]
    [Arguments(7)]
    public async Task WithArg(int x) => await Assert.That(x).IsEqualTo(7);
}

Run with --treenode-filter "/*/TUnit.TestProject.FollowupRepro7103/*/*", with and without --reflection. I logged typeof(T) and the values from each test body to see which rows ran.

Actual vs expected

Class Expected Source-gen Reflection
FxClassRowsSame 3 (1, 2, 3) 1: <Int32> value=1 1: <Int32> value=1
FxClassRowsMixed 4 2: 1, a 2: 1, a
FxMethodRowsOnGenericClass 2 1: M(1) 0
FxMixedMethodRowsOnGenericClass 3 2: M(1), M(a) 0
FxClassAndMethodRows 4 2: value=1 with x, y 2: value=1 with x, y
FxNamesClass.WithArg 1 passed failed: IndexOutOfRangeException: Index was outside the bounds of the array. in the generated __Invoke_0 passed WithArg(7)

Expected: every row runs in both modes, the same way a non-generic class or method with several rows behaves (and as #7100 does for generic methods).

Suspected cause

Source-gen: src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs, CollectConcreteInstantiations (line 656).

  • TryAddInstantiation (line 666) dedups by type combination (processedTypeCombinations, line 669). Each instantiation records the row that created it as SpecificArgumentsAttribute, and only that row is emitted as the entry's data source. Later class rows (line 790 loop and the class-level [Arguments] pass around line 900) or method rows on a generic class (the InferClassTypesFromMethodArguments pass around line 817) that infer an already-seen type are thrown away. fix(sourcegen): run every [Arguments] row that infers the same generic type #7100 adds AdditionalArgumentsAttributes for the generic-method case only. The same approach probably needs to cover class rows, method rows that infer class type args, and the class × method cartesian product.
  • FxNamesClass.WithArg: the instantiation is created from the class row (SpecificArgumentsAttribute = the class [Arguments(5)]). The method-level [Arguments(7)] is never emitted as a method data source, so the invoker reads args[0] from an empty array. (Suspected. I did not trace the emitter, but the failure is at TUnit_TestProject_FollowupRepro7103_FxNamesClass_T_WithArg__int.g.cs:37.)

Reflection: src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs, DiscoverGenericTests (line 629), and the streaming twin DiscoverGenericTestsStreamingAsync (around line 777).

  • When the generic class has no class-level data source, it returns at line 704 (and around line 886 in the streaming version) without looking at method-level rows. So method rows on a generic class never infer the class's T, and no tests are produced.
  • For class rows, the loop at line 711 builds one TestMetadata per data row (BuildTestMetadata(concreteType, resolvedMethod, dataRow)), but only one test per closed type runs. I did not trace where the duplicates collapse. It may be the test-id / class-data-source handling for the closed type.

Tests to add

Fixtures in tests/TUnit.TestProject/Bugs/<issue>/ with [EngineTest(ExpectedResult.Pass)], plus an engine test in tests/TUnit.Engine.Tests that checks exact counts in both modes, as Issue7096Tests does for #7100. Generator changes also need a snapshot in TUnit.Core.SourceGenerator.Tests.

Related: #7094, #7100, #7102, #7096

Activity

  1. added 4 commits that reference this issue on Oct 18, 2026
    9cc6b10
    9c25f3a
    083b950
    feaabd8
  2. added a commit that references this issue on Oct 19, 2026
    9af2fe3
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