Repository navigation
Generic class type arguments that violate constraints are dropped in source-gen and fail the whole assembly scan in reflection mode #7107
Description
Summary
#7102 (for #7097) makes source-gen report a failed test when a method type argument inferred from a method-level data attribute breaks the method's constraints, as reflection mode already does. These cases are still not covered:
- A class type argument inferred from a class-level row breaks the class's constraints. Source-gen drops the row without any message. Reflection mode fails discovery of the whole assembly: one
[ASSEMBLY SCAN FAILED] <assembly>test, and every source-defined test in the assembly disappears. - A class-level
[GenerateGenericTest]type breaks the class's constraints. Same as 1: source-gen drops it, and reflection fails the whole assembly scan. - A method-level row on a generic class infers a class type argument that breaks the constraints. Source-gen drops the row. Reflection runs no rows (see the generic-class rows issue).
- A method-level
[GenerateGenericTest]type breaks the method's constraints. Source-gen drops it. Reflection reports a failed[GENERIC METHOD RESOLUTION FAILED]test. IInfersType<T>([Matrix<T>]) infers a type that breaks the method's constraints. Source-gen drops the test. Reflection reports one failed test per matrix value.
None of these produce a compile-time diagnostic.
Confirmed on current main (b1cceb3), net10.0. The #7102 description says it leaves class type arguments, [GenerateGenericTest] and IInfersType<T> unchanged. I did not run its branch.
Repro
Each case was built on its own, in tests/TUnit.TestProject, because case 1 and case 2 break reflection discovery for everything else.
namespace TUnit.TestProject.FollowupRepro7103;
// Case 1: class-level rows; "x" breaks `where T : struct`
[Arguments(1)]
[Arguments("x")]
public class FxClassConstraintCtor<T>(T value) where T : struct
{
[Test]
public async Task Run() => await Assert.That(value).IsNotEqualTo(default(T));
}
// Case 1b: the only class row is invalid
[Arguments("x")]
public class FxClassConstraintOnlyInvalid<T>(T value) where T : struct
{
[Test]
public async Task Run() => await Assert.That(value).IsNotEqualTo(default(T));
}
// Case 2: class-level [GenerateGenericTest] with an invalid type
[GenerateGenericTest(typeof(int))]
[GenerateGenericTest(typeof(string))]
public class FxClassConstraintGenerateGeneric<T> where T : struct
{
[Test]
public async Task Run() => await Assert.That(typeof(T).IsValueType).IsTrue();
}
// Case 3: a method row infers the class type argument
public class FxClassConstraintMethodRow<T> where T : struct
{
[Test]
[Arguments(1)]
[Arguments("x")]
public async Task M(T value) => await Assert.That(value).IsNotEqualTo(default(T));
}
public class FxMethodConstraintOther
{
// Case 4: method-level [GenerateGenericTest] with an invalid type
[Test]
[GenerateGenericTest(typeof(int))]
[GenerateGenericTest(typeof(string))]
public async Task GenerateGeneric<T>() where T : struct
=> await Assert.That(typeof(T).IsValueType).IsTrue();
}
public class FxMethodConstraintMatrix
{
// Case 5: IInfersType<T> infers an invalid type
[Test]
[MatrixDataSource]
public async Task Matrix<T>([Matrix<string>("a", "b")] T value) where T : struct
=> await Assert.That(value).IsNotEqualTo(default(T));
// Control: valid IInfersType<T>
[Test]
[MatrixDataSource]
public async Task MatrixValid<T>([Matrix<int>(1, 2)] T value) where T : struct
=> await Assert.That(value).IsNotEqualTo(default(T));
}Actual
| Case | Source-gen | Reflection (--reflection) |
|---|---|---|
1 FxClassConstraintCtor |
1 passed (<Int32>); the "x" row is missing |
Assembly scan failed. --list-tests finds 49 tests (dynamic tests only) instead of about 18,660 |
1b FxClassConstraintOnlyInvalid |
0 tests, no message | Assembly scan failed |
2 FxClassConstraintGenerateGeneric |
1 passed (<Int32>); string is missing |
Assembly scan failed |
3 FxClassConstraintMethodRow |
1 passed M(1); the "x" row is missing |
0 tests (method rows on generic classes never run in reflection) |
4 GenerateGeneric |
1 passed GenerateGeneric<int>; string is missing |
passed GenerateGeneric<Int32>, plus a failed [GENERIC METHOD RESOLUTION FAILED] ...FxMethodConstraintOther.GenerateGeneric: [GenerateGenericTest] constraint violation on method 'GenerateGeneric': GenericArguments[0], 'System.String', ... violates the constraint |
5 Matrix |
0 tests, no message | 2 × failed Matrix: GenericTypeResolutionException: Type 'String' does not satisfy the 'struct' constraint for generic parameter 'T' |
5 MatrixValid (control) |
2 passed | 2 passed |
Reflection error for case 1:
failed [ASSEMBLY SCAN FAILED] TUnit.TestProject
InvalidOperationException: Failed to create concrete type for TUnit.TestProject.FollowupRepro7103.FxClassConstraintCtor`1.
Error: GenericArguments[0], 'System.String', on '...FxClassConstraintCtor`1[T]' violates the constraint of type 'T'.. Generic parameter count: 1, Type arguments provided: 1, Data row length: 1
at ReflectionTestDataCollector.DiscoverGenericTests(Type genericTypeDefinition) ReflectionTestDataCollector.cs:764
at ReflectionTestDataCollector.DiscoverTestsInAssembly(Assembly assembly) ReflectionTestDataCollector.cs:429
at ReflectionTestDataCollector.ProcessAssemblyAsync(Assembly assembly, SemaphoreSlim semaphore) ReflectionTestDataCollector.cs:72
---> ArgumentException at ReflectionGenericTypeResolver.CreateConcreteType ReflectionGenericTypeResolver.cs:187
Case 2 gives the same failure, with ... from [GenerateGenericTest] in the message.
Expected
Following the reasoning in #7097 and #7102: every row or type whose inferred type arguments break a constraint shows up as one failed test with the same constraint message in both modes, for example Type 'String' does not satisfy the 'struct' constraint for generic parameter 'T'. Valid rows and types in the same class keep running. A bad row must never take down discovery of the other tests in the assembly.
Suspected cause / file pointers
Reflection:
src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs,DiscoverGenericTests(line 629): thecatchblocks at line 691 (class[GenerateGenericTest]) and line 762 (class data rows) rethrow asInvalidOperationException. The exception escapesDiscoverTestsInAssemblyand is caught inProcessAssemblyAsync(line 74), which replaces the whole assembly withCreateFailedTestMetadataForAssembly(AssemblyScanFailed, line 1397). The streaming twin has the same pattern at lines 847 and 953. These should yield a failedTestMetadatafor that row or type only. AGenericTypeCreationFailedfailed-metadata helper already exists (lines 859 and 966).ReflectionGenericTypeResolver.CreateConcreteType(src/TUnit.Engine/Discovery/ReflectionGenericTypeResolver.cs:187) callsMakeGenericTypewith no up-front constraint check. A check withTestGenericTypeResolver.ValidateAgainstConstraintswording would make the message match.- Case 4 already gives a failed test, but its message is the raw
ArgumentExceptiontext, not theGenericTypeResolutionExceptionwording.
Source-gen:
src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs,TryAddInstantiation(line 666):ValidateClassTypeConstraintsfails →returnat line 680 for class args. TheValidateTypeConstraintsfailure for[GenerateGenericTest](line 926+) andIInfersType<T>(InferTypesFromTypeInferringAttributes, line 856 →TryAddInstantiationwith no specific attribute) also returns without emitting anything. fix(engine): substitute class type arguments and array element types in build-time constraint checks #7102 addsMethodConstraintViolationandGenericConstraintViolationDataSourcefor method-level data attributes only. For class args, aTestEntry<TClosed>cannot be emitted for a type that breaks the class's own constraints, so a failed entry needs a non-closed shape. One option is to register it against the open type, or as a dedicated failed-test registration, with the message baked in.- If every inferred instantiation is invalid,
concreteInstantiations.Count == 0(around line 499) emits nothing at all, which is case 1b and case 5.
Activity
fix: report generic class constraint violations as failed tests in bo…
fix: report generic class constraint violations as failed tests in bo…