You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Single tuple parameter plus a CancellationToken fails in reflection mode, and a leading token fails for non-generic methods #7173
A test whose only data parameter is a single tuple, and which also declares a CancellationToken, does not run in reflection mode. The rows come from a [MethodDataSource] that returns IEnumerable<(int, int)>, for example:
InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Threading.CancellationToken'
ArgumentException: Object of type 'System.Int32' cannot be converted to type 'System.ValueTuple2[System.Int32,System.Int32]'`
The same methods without the CancellationToken pass in both modes (Single_Tuple_Parameter_Is_Not_Spread and NonGeneric_Single_Tuple_Parameter_Is_Not_Spread in tests/TUnit.TestProject/Bugs/_7108/GenericMethodNonTupleMethodDataSourceTests.cs).
Where
Generic methods in reflection mode: TestGenericTypeResolver.ResolveMethodGenericArguments (src/TUnit.Engine/Services/TestGenericTypeResolver.cs, around line 420). The row arrives as its unwrapped elements (25, 26), and with a token present the resolver maps the first element to the tuple parameter (T, T) instead of rebuilding the tuple, so it throws at line 555.
Non-generic methods with a token before the tuple: both modes bind the row positionally and pass a row value to the token parameter.
Analyzer TUnit0001 also reports the leading-token methods (Attribute argument types 'int, int' don't match method parameter types 'CancellationToken, (T, T)'), because WithoutCancellationTokenParameter (src/TUnit.Analyzers/Extensions/ParameterExtensions.cs) only removes a trailing token. So a leading token is either unsupported, and the analyzer is right, or supported, and the analyzer and the non-generic path are wrong. The generator already puts a leading token back at its declared position for generic methods.
How to reproduce
Add the methods above to a test class in TUnit.TestProject (wrap the two leading-token methods in #pragma warning disable TUnit0001), build, and run with a --treenode-filter for that class, once with --reflection and once without.
Expected
A single tuple parameter receives the whole row in both modes, wherever the CancellationToken is declared, and TUnit0001 agrees with what the engine accepts.
One more case of the analyzer gap: TUnit0001 also flags [Arguments(1, null)] on Run<T>(T first, CancellationToken token, T second), because WithoutCancellationTokenParameter strips only a trailing token. Both engine modes run that row (after #7198, as Run<object>). The fixture Bugs/_7155/NullAndValueTypeEdgeCaseTests.cs in #7198 suppresses TUnit0001 for now.
A related case for generic classes, found while reviewing #7203. When class type arguments are inferred from a method [Arguments] row, values are matched to parameters by declared index, and a leading CancellationToken is skipped without shifting that index. In both modes, class C<T> { [Test, Arguments(1)] public void Run(CancellationToken token, T value) {} } therefore infers nothing, and the row is dropped without any message.
Generator: InferClassTypesFromMethodArguments in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs (the methodParam.Type.Name == "CancellationToken"continue keeps argIndex aligned to the declared parameters).
Reflection: InferClassTypeArgumentsFromMethodRow in src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs and InferClassTypeArguments in ReflectionGenericConstraintViolations.cs (parameters[i] against values[i]).
Expected: data values are matched to the data-bound parameters with tokens filtered out first, as the generic-method inference path already does.
Activity
fix(sourcegen): keep a leading CancellationToken before a single tupl…
thomhurst commentedon Oct 20, 2026
One more case of the analyzer gap: TUnit0001 also flags
[Arguments(1, null)]onRun<T>(T first, CancellationToken token, T second), becauseWithoutCancellationTokenParameterstrips only a trailing token. Both engine modes run that row (after #7198, asRun<object>). The fixtureBugs/_7155/NullAndValueTypeEdgeCaseTests.csin #7198 suppresses TUnit0001 for now.thomhurst commentedon Oct 20, 2026
A related case for generic classes, found while reviewing #7203. When class type arguments are inferred from a method
[Arguments]row, values are matched to parameters by declared index, and a leadingCancellationTokenis skipped without shifting that index. In both modes,class C<T> { [Test, Arguments(1)] public void Run(CancellationToken token, T value) {} }therefore infers nothing, and the row is dropped without any message.InferClassTypesFromMethodArgumentsinsrc/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs(themethodParam.Type.Name == "CancellationToken"continuekeepsargIndexaligned to the declared parameters).InferClassTypeArgumentsFromMethodRowinsrc/TUnit.Engine/Discovery/ReflectionTestDataCollector.csandInferClassTypeArgumentsinReflectionGenericConstraintViolations.cs(parameters[i]againstvalues[i]).Expected: data values are matched to the data-bound parameters with tokens filtered out first, as the generic-method inference path already does.
test: cover tokens interleaved with a spread row, and a generic tuple…
fix: keep trailing-array tuple elements and skip tokens in generic in…
fix: skip CancellationToken parameters in reflection MethodDataSource…
test: drop the reflection-mode skip now that #7173 is fixed
fix(sourcegen): infer generic method types from any MethodDataSource …
fix: bind a CancellationToken declared anywhere and keep a single tup…