Repository navigation
Generic test classes drop [Arguments] rows that infer the same type arguments (both modes; reflection runs no method rows) #7106
Description
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 sameTrun 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'sT) 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 withIndexOutOfRangeExceptionin 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 asSpecificArgumentsAttribute, 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 (theInferClassTypesFromMethodArgumentspass 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 addsAdditionalArgumentsAttributesfor 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 readsargs[0]from an empty array. (Suspected. I did not trace the emitter, but the failure is atTUnit_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
TestMetadataper 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.
Activity
fix(engine): run every [Arguments] row on generic test classes in bot…
fix(engine): run every [Arguments] row on generic test classes in bot…
fix(engine): run every [Arguments] row on generic test classes in bot…
fix(engine): run every [Arguments] row on generic test classes in bot…
fix(engine): run every [Arguments] row on generic test classes in bot…