Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,11 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
// - null (passed as null, not as array with null element)
// - T[] (passed directly, not wrapped in another array)
// - T (wrapped in array with single element)
// Anything else goes through CastHelper.ToTrailingArray, which also converts an
// array of a different runtime type element-wise (e.g. an object[] supplied by a
// MatrixAttribute subclass for a MyEnum[] parameter — issue #6678).
var singleArg = $"{argumentsArrayName}[{regularParamCount}]";
var checkAndCast = $"({singleArg} is null ? null : {singleArg} is {paramsParam.Type.GloballyQualified()} arr ? arr : new {elementType.GloballyQualified()}[] {{ global::TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({singleArg}) }})";
var checkAndCast = $"({singleArg} is null ? null : {singleArg} is {paramsParam.Type.GloballyQualified()} arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray<{elementType.GloballyQualified()}>({singleArg}))";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
argumentExpressions.Add(checkAndCast);
}
else
Expand Down
35 changes: 35 additions & 0 deletions src/TUnit.Core/Helpers/CastHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,41 @@ public static class CastHelper
return (T?)result;
}

/// <summary>
/// Binds a single non-null argument to a trailing <c>T[]</c> parameter when the argument is not
/// already a <c>T[]</c>. Applies C# params expansion first (a <typeparamref name="T"/> value becomes
/// a one-element array), then converts any other array element-wise, and otherwise wraps the
/// converted scalar.
/// </summary>
/// <remarks>
/// The element-wise branch exists because data sources often supply arrays whose runtime type is
/// not the parameter's — e.g. an <c>object[]</c> from a custom <see cref="MatrixAttribute"/> for a
/// <c>MyEnum[]</c> parameter (issue #6678). Without it the whole array would be forced into a single
/// element and fail to cast. The <typeparamref name="T"/> check runs first so a <c>params object[]</c>
/// parameter still receives an <c>int[]</c> argument as one element, matching C#.
/// AOT-safe: allocates via <c>new T[]</c> and converts through <see cref="Cast{T}"/>.
/// </remarks>
public static T[] ToTrailingArray<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(object value)
{
if (value is T element)
{
return [element];
}

if (value is Array { Rank: 1 } source)
{
var result = new T[source.Length];
for (var i = 0; i < result.Length; i++)
{
result[i] = Cast<T>(source.GetValue(i))!;
Comment thread
thomhurst marked this conversation as resolved.
Outdated
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
}

return result;
}

return [Cast<T>(value)!];
}

/// <summary>
/// Returns the value as-is if it's null or already assignable to the target type;
/// otherwise delegates to <see cref="Cast"/> to apply conversion operators.
Expand Down
27 changes: 21 additions & 6 deletions src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1914,21 +1914,36 @@ private static bool MatchesCovariantInterface(Type paramType, Type paramGenericD
}
else
{
// Single non-array argument - wrap in array
var singleElementArray = Array.CreateInstance(paramsElementType, 1);
if (paramsElementType.IsAssignableFrom(singleArg.GetType()))
// Mirrors CastHelper.ToTrailingArray used by source-generated invokers.
var singleArgType = singleArg.GetType();
if (paramsElementType.IsAssignableFrom(singleArgType) || IsCovariantCompatible(paramsElementType, singleArgType))
{
// A single element of the array's type - wrap (C# params expansion).
// Checked before the Array branch so `params object[]` still
// receives an int[] as one element, like C#.
var singleElementArray = Array.CreateInstance(paramsElementType, 1);
singleElementArray.SetValue(singleArg, 0);
castedArgs[regularParamsCount] = singleElementArray;
}
else if (IsCovariantCompatible(paramsElementType, singleArg.GetType()))
else if (singleArg is Array { Rank: 1 } sourceArray)
{
singleElementArray.SetValue(singleArg, 0);
// An array whose runtime type isn't the parameter's (e.g. an object[]
// from a MatrixAttribute subclass for a MyEnum[] parameter, issue #6678):
// convert element-wise rather than forcing it into a single element.
var convertedArray = Array.CreateInstance(paramsElementType, sourceArray.Length);
for (var i = 0; i < sourceArray.Length; i++)
{
convertedArray.SetValue(CastHelper.Cast(paramsElementType, sourceArray.GetValue(i)), i);
}
castedArgs[regularParamsCount] = convertedArray;
}
else
{
// Single scalar needing conversion - convert, then wrap in array
var singleElementArray = Array.CreateInstance(paramsElementType, 1);
singleElementArray.SetValue(CastHelper.Cast(paramsElementType, singleArg), 0);
castedArgs[regularParamsCount] = singleElementArray;
}
castedArgs[regularParamsCount] = singleElementArray;
}
}
else
Expand Down
Loading
Loading