Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
private const string InstanceMethodDataSourceAttributeName = "global::TUnit.Core.InstanceMethodDataSourceAttribute";

private static readonly DiagnosticDescriptor GenerationErrorDescriptor = new(
"TUNIT0999",

Check warning on line 29 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / integration-tests

Enable analyzer release tracking for the analyzer project containing rule 'TUNIT0999' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 29 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / doc-snippets

Enable analyzer release tracking for the analyzer project containing rule 'TUNIT0999' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 29 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (tr-TR)

'TUNIT0999' kuralını içeren çözümleyici projesi için çözümleyici yayını izlemeyi etkinleştir (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 29 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNIT0999' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 29 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNIT0999' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 29 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNIT0999' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 29 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNIT0999' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Source Generation Error",
"Failed to generate test metadata for {0}.{1}: {2}",
"TUnit",
Expand Down Expand Up @@ -4516,9 +4516,10 @@
{
writer.AppendLine("// Special handling for single tuple parameter");
// Factories preserve one-element tuple rows, so a matching tuple is the whole row,
// even when its element is another tuple of the same type.
// even when its element is another tuple of the same type. Incompatible singleton
// containers also need element-wise conversion through the wrapped-row path.
var unwrappedCheck = singleTupleParam.TupleElements.Length == 1
? $" && args[0] is not {singleTupleParam.GloballyQualified()}"
? " && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 }"
Comment thread
thomhurst marked this conversation as resolved.
Resolved
: "";
writer.AppendLine($"if (args.Length == {singleTupleParam.TupleElements.Length.ToString(CultureInfo.InvariantCulture)}{unwrappedCheck})");
writer.AppendLine("{");
Expand Down Expand Up @@ -7699,7 +7700,7 @@
{
if (!inferredTypes.ContainsKey(name))
{
inferredTypes[name] = compilation.GetSpecialType(SpecialType.System_Object);

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / integration-tests

Dereference of a possibly null reference.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / doc-snippets

Dereference of a possibly null reference.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (tr-TR)

Olası bir null başvurunun başvurma işlemi.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Dereference of a possibly null reference.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Dereference of a possibly null reference.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Dereference of a possibly null reference.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Dereference of a possibly null reference.

Check warning on line 7703 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Dereference of a possibly null reference.
}
}
}
Expand Down
47 changes: 47 additions & 0 deletions src/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ internal InstanceMethodDataSourceAttribute ToInstanceVariant()
// Use MembersToGenerate which contains the correct parameters for this data generator type
// (constructor params for class-level, method params for method-level)
var paramTypes = GetMemberTypes(dataGeneratorMetadata.MembersToGenerate);
var genericTupleParameter = GetSingleErasedGenericTupleParameter(dataGeneratorMetadata.MembersToGenerate);

object?[] ToRow(object? item)
{
Expand All @@ -355,6 +356,24 @@ internal InstanceMethodDataSourceAttribute ToInstanceVariant()
return [item];
}
}

// Retain nested value tuples, or all reference-tuple elements, until binding.
if (genericTupleParameter is not null)
{
item = DataSourceHelpers.InvokeIfFunc(item);
if (DataSourceHelpers.IsTuple(item))
{
return DataSourceHelpers.ShouldFlattenErasedTuple(item!, genericTupleParameter)
? DataSourceHelpers.UnwrapTupleAot(item)
: [item];
}

// Invoke only the outer factory, matching ToObjectArrayWithTypes.
return item is object?[] array && item.GetType() == typeof(object[])
? array
: [item];
}

return item.ToObjectArrayWithTypes(paramTypes);
}

Expand Down Expand Up @@ -776,6 +795,34 @@ private static bool IsNumericWidening(Type from, Type to)
return args;
}

private static ParameterMetadata? GetSingleErasedGenericTupleParameter(IMemberMetadata[]? members)
{
ParameterMetadata? dataParameter = null;
foreach (var member in members ?? [])
{
if (member is not ParameterMetadata parameter)
{
return null;
}

if (parameter.Type == typeof(CancellationToken))
{
continue;
}

if (dataParameter is not null)
{
return null;
}

dataParameter = parameter;
}

return dataParameter is not null && DataSourceHelpers.IsErasedGenericTupleParameter(dataParameter)
? dataParameter
: null;
}

private static Type[]? GetMemberTypes(IMemberMetadata[]? members)
{
if (members == null || members.Length == 0)
Expand Down
33 changes: 33 additions & 0 deletions src/TUnit.Core/Helpers/DataSourceHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,39 @@ namespace TUnit.Core.Helpers;
/// </summary>
public static class DataSourceHelpers
{
internal static bool IsErasedGenericTupleParameter(ParameterMetadata parameter)
=> parameter.Type == typeof(object)
&& parameter.TypeInfo is ConstructedGeneric generic
&& generic.GenericDefinition.FullName is { } name
&& (name.StartsWith("System.ValueTuple`", StringComparison.Ordinal)
|| name.StartsWith("System.Tuple`", StringComparison.Ordinal));

// Reference-tuple targets and sources need every element for invoker conversions. Value tuples can
// retain their nested shape unless mutable element boxes must reach invocation.
internal static bool ShouldFlattenErasedTuple(object tuple, ParameterMetadata parameter)
{
var targetName = (parameter.TypeInfo as ConstructedGeneric)?.GenericDefinition.FullName;
var targetIsReferenceTuple = targetName?.StartsWith("System.Tuple`", StringComparison.Ordinal) == true;

// A flattened [null] is indistinguishable from a null row. The reference-tuple
// invoker can reconstruct a preserved singleton containing null, including conversions.
if (targetName == "System.Tuple`1" && UnwrapTupleAot(tuple) is [null])
{
return false;
}

// Keep singleton value-tuple rows wrapped even across tuple families. An element
// may itself be a singleton tuple; flattening would lose which container is the row.
if (targetName == "System.ValueTuple`1")
{
return RequiresFlattenedTupleBinding(tuple);
}

return targetIsReferenceTuple
|| !tuple.GetType().IsValueType
|| RequiresFlattenedTupleBinding(tuple);
Comment thread
thomhurst marked this conversation as resolved.
Resolved
}

private static readonly ConcurrentDictionary<Type, bool> TupleMutableElementShapes = new();

// Property injection, initialization, and event callbacks mutate boxed structs.
Expand Down
7 changes: 6 additions & 1 deletion src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3917,12 +3917,17 @@ private static object ConstructInvocationTuple(Type tupleType, object?[] element
var singleDataIndex = tupleElements is not null
? Array.FindIndex(parameters, static p => p.ParameterType != typeof(CancellationToken))
: -1;
// Singleton value-tuple rows stay wrapped through DataUnwrapper, including
// incompatible containers whose elements need conversion. Match the generated
// invoker instead of treating that container as an already-flattened element.
if (tupleElements is { Length: > 0 }
&& args.Length == tupleElements.Length + parameters.Length - 1
&& (tupleElements.Length > 1
|| args[singleDataIndex] is { } singleValue
&& !(DataSourceHelpers.IsTuple(singleValue) && DataSourceHelpers.UnwrapTupleAot(singleValue) is [null])
&& !parameters[singleDataIndex].ParameterType.IsInstanceOfType(singleValue)))
&& !parameters[singleDataIndex].ParameterType.IsInstanceOfType(singleValue)
&& !(parameters[singleDataIndex].ParameterType.IsValueType
&& DataSourceHelpers.IsTuple(singleValue) && DataSourceHelpers.UnwrapTupleAot(singleValue).Length == 1)))
{
// DataUnwrapper spreads an incompatible tuple so each element can be
// converted before constructing the target, as the generated invoker does.
Expand Down
19 changes: 17 additions & 2 deletions src/TUnit.Engine/Helpers/DataUnwrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,31 @@ public static (object?[] Data, TestDataRowMetadata? Metadata) UnwrapWithTypesAnd
return DataSourceHelpers.UnwrapTupleAot(values[0]);
}

var lastDataParameterType = expectedParameters[lastDataParameterIndex].Type;
var lastDataParameter = expectedParameters[lastDataParameterIndex];
var lastDataParameterType = lastDataParameter.Type;

// Generated metadata erases a parameter containing type parameters to object,
// but TypeInfo retains its tuple shape. Preserve all elements until the invoker
// closes the type; reference tuples and mutable elements need flattened binding.
if (dataParameterCount == 1
&& DataSourceHelpers.IsErasedGenericTupleParameter(lastDataParameter))
{
return DataSourceHelpers.ShouldFlattenErasedTuple(values[0]!, lastDataParameter)
? DataSourceHelpers.UnwrapTupleAot(values[0])
: values;
}

// Keep compatible tuples whole (including one-element tuples holding null).
// Otherwise the invoker needs every element to reconstruct the target tuple
// with per-element conversions, rather than casting incompatible tuple types.
if (dataParameterCount == 1 && TupleHelper.IsTupleType(lastDataParameterType))
{
// Keep the runtime tuple intact until generic inference closes the parameter type.
// Singleton value-tuple conversion also needs the outer container: its element
// can itself be a tuple, indistinguishable from a preserved row after flattening.
if (!DataSourceHelpers.RequiresFlattenedTupleBinding(values[0]!)
&& (lastDataParameterType.ContainsGenericParameters || lastDataParameterType.IsInstanceOfType(values[0])))
&& (lastDataParameterType.ContainsGenericParameters || lastDataParameterType.IsInstanceOfType(values[0])
|| lastDataParameterType.IsGenericType && lastDataParameterType.GetGenericTypeDefinition() == typeof(ValueTuple<>)))
{
return values;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ internal static class TUnit_TestProject_Bugs__7440_RequiredConstructorArgumentTe
Name = "Value",
IsStatic = false,
IsNullable = false,
Getter = o => ((dynamic)o).Value,
Getter = global::TUnit.Core.PropertyMetadataFactory.CreateGenericGetter(typeof(global::TUnit.TestProject.Bugs._7440.RequiredConstructorArgumentTests<>), "Value"),
ClassMetadata = null!,
ContainingTypeMetadata = null!
},
Expand All @@ -211,7 +211,7 @@ internal static class TUnit_TestProject_Bugs__7440_RequiredConstructorArgumentTe
Name = "Number",
IsStatic = false,
IsNullable = false,
Getter = o => ((dynamic)o).Number,
Getter = global::TUnit.Core.PropertyMetadataFactory.CreateGenericGetter(typeof(global::TUnit.TestProject.Bugs._7440.RequiredConstructorArgumentTests<>), "Number"),
ClassMetadata = null!,
ContainingTypeMetadata = null!
}
Expand Down Expand Up @@ -933,7 +933,7 @@ internal static class TUnit_TestProject_Bugs__7440_SingleGenericRequiredFieldTes
Name = "Property",
IsStatic = false,
IsNullable = false,
Getter = o => ((dynamic)o).Property,
Getter = global::TUnit.Core.PropertyMetadataFactory.CreateGenericGetter(typeof(global::TUnit.TestProject.Bugs._7440.SingleGenericRequiredFieldTests<>), "Property"),
ClassMetadata = null!,
ContainingTypeMetadata = null!
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ internal static class TUnit_TestProject_Bugs__7499_GenericPropertyMetadataTests_
private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("MetadataGettersReadClosedInstance", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata);
private static global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<string> __CreateInstance_0(global::System.Type[] typeArgs, object?[] args)
{
return (global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<string>)global::System.Activator.CreateInstance(typeof(global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<string>), args)!;
return new global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<string>(global::TUnit.Core.Helpers.CastHelper.Cast<string>(args[0]));
}
private static global::System.Threading.Tasks.ValueTask __Invoke_0(global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<string> instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -106,7 +106,7 @@ internal static class TUnit_TestProject_Bugs__7499_GenericPropertyMetadataTests_
};
private static global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<int> __CreateInstance_1(global::System.Type[] typeArgs, object?[] args)
{
return (global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<int>)global::System.Activator.CreateInstance(typeof(global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<int>), new object[] { 42 })!;
return new global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<int>(global::TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]));
}
private static global::System.Threading.Tasks.ValueTask __Invoke_1(global::TUnit.TestProject.Bugs._7499.GenericPropertyMetadataTests<int> instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "value", new
try
{
// Special handling for single tuple parameter
if (args.Length == 1 && args[0] is not global::System.ValueTuple<object>)
if (args.Length == 1 && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 })
{
// Arguments are unwrapped tuple elements, reconstruct the tuple
return new global::System.Threading.Tasks.ValueTask(instance.GenericNestedElement<global::System.ValueTuple<object>>(new global::System.ValueTuple<object>(global::TUnit.Core.Helpers.CastHelper.Cast<object>(args[0]))));
Expand Down Expand Up @@ -128,7 +128,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "value", new
try
{
// Special handling for single tuple parameter
if (args.Length == 1 && args[0] is not global::System.ValueTuple<object>)
if (args.Length == 1 && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 })
{
// Arguments are unwrapped tuple elements, reconstruct the tuple
return new global::System.Threading.Tasks.ValueTask(instance.GenericNestedElementFromInstance<global::System.ValueTuple<object>>(new global::System.ValueTuple<object>(global::TUnit.Core.Helpers.CastHelper.Cast<object>(args[0]))));
Expand Down Expand Up @@ -224,7 +224,7 @@ internal static class TUnit_TestProject_OneElementValueTupleTests__TestSource
case 0:
{
// Special handling for single tuple parameter
if (args.Length == 1 && args[0] is not global::System.ValueTuple<string>)
if (args.Length == 1 && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 })
{
// Arguments are unwrapped tuple elements, reconstruct the tuple
return new global::System.Threading.Tasks.ValueTask(instance.SingleElementFromInstance(new global::System.ValueTuple<string>(global::TUnit.Core.Helpers.CastHelper.Cast<string>(args[0]))));
Expand All @@ -242,7 +242,7 @@ internal static class TUnit_TestProject_OneElementValueTupleTests__TestSource
case 1:
{
// Special handling for single tuple parameter
if (args.Length == 1 && args[0] is not global::System.ValueTuple<string>)
if (args.Length == 1 && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 })
{
// Arguments are unwrapped tuple elements, reconstruct the tuple
return new global::System.Threading.Tasks.ValueTask(instance.SingleElement(new global::System.ValueTuple<string>(global::TUnit.Core.Helpers.CastHelper.Cast<string>(args[0]))));
Expand All @@ -260,7 +260,7 @@ internal static class TUnit_TestProject_OneElementValueTupleTests__TestSource
case 2:
{
// Special handling for single tuple parameter
if (args.Length == 1 && args[0] is not global::System.ValueTuple<object>)
if (args.Length == 1 && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 })
{
// Arguments are unwrapped tuple elements, reconstruct the tuple
return new global::System.Threading.Tasks.ValueTask(instance.NestedElement(new global::System.ValueTuple<object>(global::TUnit.Core.Helpers.CastHelper.Cast<object>(args[0]))));
Expand All @@ -278,7 +278,7 @@ internal static class TUnit_TestProject_OneElementValueTupleTests__TestSource
case 3:
{
// Special handling for single tuple parameter
if (args.Length == 1 && args[0] is not global::System.ValueTuple<object>)
if (args.Length == 1 && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 })
{
// Arguments are unwrapped tuple elements, reconstruct the tuple
return new global::System.Threading.Tasks.ValueTask(instance.NestedElementFromInstance(new global::System.ValueTuple<object>(global::TUnit.Core.Helpers.CastHelper.Cast<object>(args[0]))));
Expand All @@ -296,7 +296,7 @@ internal static class TUnit_TestProject_OneElementValueTupleTests__TestSource
case 4:
{
// Special handling for single tuple parameter
if (args.Length == 1 && args[0] is not global::System.ValueTuple<object>)
if (args.Length == 1 && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 })
{
// Arguments are unwrapped tuple elements, reconstruct the tuple
return new global::System.Threading.Tasks.ValueTask(instance.WrappedNestedElement(new global::System.ValueTuple<object>(global::TUnit.Core.Helpers.CastHelper.Cast<object>(args[0]))));
Expand All @@ -314,7 +314,7 @@ internal static class TUnit_TestProject_OneElementValueTupleTests__TestSource
case 5:
{
// Special handling for single tuple parameter
if (args.Length == 1 && args[0] is not global::System.ValueTuple<object>)
if (args.Length == 1 && args[0] is not global::System.Runtime.CompilerServices.ITuple { Length: 1 })
{
// Arguments are unwrapped tuple elements, reconstruct the tuple
return new global::System.Threading.Tasks.ValueTask(instance.WrappedNestedElementFromInstance(new global::System.ValueTuple<object>(global::TUnit.Core.Helpers.CastHelper.Cast<object>(args[0]))));
Expand Down
Loading
Loading