From 218f076dfbfb97368d190b2ad3c9fb2d33ea96d0 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:42:35 +0100 Subject: [PATCH 1/2] fix: convert foreign array element-wise for trailing array parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since #6122 a trailing `T[]` parameter binds like `params`: a single argument that is not already a `T[]` was wrapped as one element via `Cast`. Data sources often supply an array of a different runtime type — e.g. an `object[]` from a MatrixAttribute subclass for a `MyEnum[]` parameter — which then failed with an InvalidCastException in both execution modes. Add `CastHelper.ToTrailingArray` (AOT-safe: `new T[]` + `Cast` per element) and use it from the generated single-argument branch; mirror the same rule in ReflectionTestDataCollector. Order matters: a value that is itself a `T` still wraps (C# params expansion, so `params object[]` keeps an `int[]` as one element); only an array that is not a `T` is converted element-wise. Closes #6678 Claude-Session: https://claude.ai/code/session_015Xx3PGYRU1KjEuEmzXMMpj --- .../Helpers/TupleArgumentHelper.cs | 5 +- src/TUnit.Core/Helpers/CastHelper.cs | 35 +++ .../Discovery/ReflectionTestDataCollector.cs | 27 ++- .../ArgsAsArrayTests.Test.verified.txt | 207 +++++++++++++++++- ...nflictingNamespace.DotNet10_0.verified.txt | 2 +- ...onflictingNamespace.DotNet8_0.verified.txt | 2 +- ...onflictingNamespace.DotNet9_0.verified.txt | 2 +- ...thConflictingNamespace.Net4_7.verified.txt | 2 +- ...nflictingNamespace.DotNet10_0.verified.txt | 2 +- ...onflictingNamespace.DotNet8_0.verified.txt | 2 +- ...onflictingNamespace.DotNet9_0.verified.txt | 2 +- ...thConflictingNamespace.Net4_7.verified.txt | 2 +- .../DataDrivenTests.Test.verified.txt | 2 +- ...hodDataSourceDrivenTests.Test.verified.txt | 2 +- .../Tests2112.Test.verified.txt | 4 +- .../Tests6150.Test.verified.txt | 2 +- ...Has_No_API_Changes.DotNet10_0.verified.txt | 1 + ..._Has_No_API_Changes.DotNet8_0.verified.txt | 1 + ..._Has_No_API_Changes.DotNet9_0.verified.txt | 1 + ...ary_Has_No_API_Changes.Net4_7.verified.txt | 1 + tests/TUnit.TestProject/ArgsAsArrayTests.cs | 51 +++++ 21 files changed, 326 insertions(+), 29 deletions(-) diff --git a/src/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs b/src/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs index 5f3fd464278..98186c628d0 100644 --- a/src/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs +++ b/src/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TupleArgumentHelper.cs @@ -146,8 +146,11 @@ public static List GenerateArgumentAccessWithParams(IList({singleArg}) }})"; + var checkAndCast = $"({singleArg} is null ? null : {singleArg} is {paramsParam.Type.GloballyQualified()} arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray<{elementType.GloballyQualified()}>({singleArg}))"; argumentExpressions.Add(checkAndCast); } else diff --git a/src/TUnit.Core/Helpers/CastHelper.cs b/src/TUnit.Core/Helpers/CastHelper.cs index b7794ff6934..c84875c2b7a 100644 --- a/src/TUnit.Core/Helpers/CastHelper.cs +++ b/src/TUnit.Core/Helpers/CastHelper.cs @@ -29,6 +29,41 @@ public static class CastHelper return (T?)result; } + /// + /// Binds a single non-null argument to a trailing T[] parameter when the argument is not + /// already a T[]. Applies C# params expansion first (a value becomes + /// a one-element array), then converts any other array element-wise, and otherwise wraps the + /// converted scalar. + /// + /// + /// The element-wise branch exists because data sources often supply arrays whose runtime type is + /// not the parameter's — e.g. an object[] from a custom for a + /// MyEnum[] parameter (issue #6678). Without it the whole array would be forced into a single + /// element and fail to cast. The check runs first so a params object[] + /// parameter still receives an int[] argument as one element, matching C#. + /// AOT-safe: allocates via new T[] and converts through . + /// + 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(source.GetValue(i))!; + } + + return result; + } + + return [Cast(value)!]; + } + /// /// Returns the value as-is if it's null or already assignable to the target type; /// otherwise delegates to to apply conversion operators. diff --git a/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs b/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs index a67f48226e3..fa5e12e0979 100644 --- a/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs +++ b/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs @@ -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 diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt index 2ca9f8a92a2..a2ad5867cd6 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt @@ -55,6 +55,18 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne private static readonly global::TUnit.Core.MethodMetadata __mm_8 = global::TUnit.Core.MethodMetadataFactory.Create("NonParamsStringArray_BeyondStaticCaseCap", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] { global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", new global::TUnit.Core.ConcreteType(typeof(string[])), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.ArgsAsArrayTests).GetMethod("NonParamsStringArray_BeyondStaticCaseCap", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(string[]) }, null)!.GetParameters()[0]) +}); + private static readonly global::TUnit.Core.MethodMetadata __mm_9 = global::TUnit.Core.MethodMetadataFactory.Create("TrailingArray_ObjectArrayFromMatrix", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] +{ +global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProject.ConfigKind[]), "configs", new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.ConfigKind[])), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.ArgsAsArrayTests).GetMethod("TrailingArray_ObjectArrayFromMatrix", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(global::TUnit.TestProject.ConfigKind[]) }, null)!.GetParameters()[0]) +}); + private static readonly global::TUnit.Core.MethodMetadata __mm_10 = global::TUnit.Core.MethodMetadataFactory.Create("TrailingParamsArray_ObjectArrayFromMatrix", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] +{ +global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProject.ConfigKind[]), "configs", new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.ConfigKind[])), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.ArgsAsArrayTests).GetMethod("TrailingParamsArray_ObjectArrayFromMatrix", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(global::TUnit.TestProject.ConfigKind[]) }, null)!.GetParameters()[0]) +}); + private static readonly global::TUnit.Core.MethodMetadata __mm_11 = global::TUnit.Core.MethodMetadataFactory.Create("ParamsObjectArray_ArrayArgumentStaysSingleElement", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] +{ +global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", new global::TUnit.Core.ConcreteType(typeof(object[])), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.ArgsAsArrayTests).GetMethod("ParamsObjectArray_ArrayArgumentStaysSingleElement", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(object[]) }, null)!.GetParameters()[0]) }); private static global::TUnit.TestProject.ArgsAsArrayTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) { @@ -77,7 +89,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 1: { - instance.Params((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.Params((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0]))); return default(global::System.Threading.Tasks.ValueTask); } case 2: @@ -128,7 +140,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 1: { - instance.ParamsEnumerable((args[0] is null ? null : args[0] is global::System.Collections.Generic.IEnumerable arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.ParamsEnumerable((args[0] is null ? null : args[0] is global::System.Collections.Generic.IEnumerable arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0]))); return default(global::System.Threading.Tasks.ValueTask); } case 2: @@ -179,7 +191,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 2: { - instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is global::System.Collections.Generic.IEnumerable arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + instance.Following_Non_Params(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is global::System.Collections.Generic.IEnumerable arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[1]))); return default(global::System.Threading.Tasks.ValueTask); } case 3: @@ -233,7 +245,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.NonParamsStringArray((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.NonParamsStringArray((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { @@ -276,7 +288,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.NonParamsStringArray_SingleValue((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.NonParamsStringArray_SingleValue((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { @@ -319,7 +331,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.NonParamsStringArray_MultipleValues((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.NonParamsStringArray_MultipleValues((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { @@ -362,7 +374,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.NonParamsIntArray((args[0] is null ? null : args[0] is int[] arr ? arr : new int[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.NonParamsIntArray((args[0] is null ? null : args[0] is int[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { @@ -405,7 +417,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.GenericStringArray((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.GenericStringArray((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { @@ -448,7 +460,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.NonParamsStringArray_BeyondStaticCaseCap((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.NonParamsStringArray_BeyondStaticCaseCap((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { @@ -479,6 +491,135 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex)); } } + case 9: + { + try + { + switch (args.Length) + { + case 0: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_ObjectArrayFromMatrix(global::System.Array.Empty())); + } + case 1: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_ObjectArrayFromMatrix((args[0] is null ? null : args[0] is global::TUnit.TestProject.ConfigKind[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); + } + case 2: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + } + case 3: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) })); + } + case 4: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) })); + } + case 5: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) })); + } + case 6: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) })); + } + default: + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_ObjectArrayFromMatrix((args.Length > 0 ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.Range(0, args.Length), i => global::TUnit.Core.Helpers.CastHelper.Cast(args[i]))) : global::System.Array.Empty()))); + } + } + catch (global::System.Exception ex) + { + return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex)); + } + } + case 10: + { + try + { + switch (args.Length) + { + case 0: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingParamsArray_ObjectArrayFromMatrix(global::System.Array.Empty())); + } + case 1: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingParamsArray_ObjectArrayFromMatrix((args[0] is null ? null : args[0] is global::TUnit.TestProject.ConfigKind[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); + } + case 2: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingParamsArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + } + case 3: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingParamsArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) })); + } + case 4: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingParamsArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) })); + } + case 5: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingParamsArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) })); + } + case 6: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingParamsArray_ObjectArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) })); + } + default: + return new global::System.Threading.Tasks.ValueTask(instance.TrailingParamsArray_ObjectArrayFromMatrix((args.Length > 0 ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.Range(0, args.Length), i => global::TUnit.Core.Helpers.CastHelper.Cast(args[i]))) : global::System.Array.Empty()))); + } + } + catch (global::System.Exception ex) + { + return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex)); + } + } + case 11: + { + try + { + switch (args.Length) + { + case 0: + { + return new global::System.Threading.Tasks.ValueTask(instance.ParamsObjectArray_ArrayArgumentStaysSingleElement(global::System.Array.Empty())); + } + case 1: + { + return new global::System.Threading.Tasks.ValueTask(instance.ParamsObjectArray_ArrayArgumentStaysSingleElement((args[0] is null ? null : args[0] is object[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); + } + case 2: + { + return new global::System.Threading.Tasks.ValueTask(instance.ParamsObjectArray_ArrayArgumentStaysSingleElement(new object[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + } + case 3: + { + return new global::System.Threading.Tasks.ValueTask(instance.ParamsObjectArray_ArrayArgumentStaysSingleElement(new object[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) })); + } + case 4: + { + return new global::System.Threading.Tasks.ValueTask(instance.ParamsObjectArray_ArrayArgumentStaysSingleElement(new object[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) })); + } + case 5: + { + return new global::System.Threading.Tasks.ValueTask(instance.ParamsObjectArray_ArrayArgumentStaysSingleElement(new object[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) })); + } + case 6: + { + return new global::System.Threading.Tasks.ValueTask(instance.ParamsObjectArray_ArrayArgumentStaysSingleElement(new object[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) })); + } + default: + return new global::System.Threading.Tasks.ValueTask(instance.ParamsObjectArray_ArrayArgumentStaysSingleElement((args.Length > 0 ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.Range(0, args.Length), i => global::TUnit.Core.Helpers.CastHelper.Cast(args[i]))) : global::System.Array.Empty()))); + } + } + catch (global::System.Exception ex) + { + return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex)); + } + } default: throw new global::System.ArgumentOutOfRangeException(nameof(methodIndex)); } @@ -645,6 +786,54 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "names", ne methodIndex: 8, createAttributes: __Attributes, attributeGroupIndex: 0), + global::TUnit.Core.TestEntryFactory.Create( + methodName: "TrailingArray_ObjectArrayFromMatrix", + fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.TrailingArray_ObjectArrayFromMatrix", + filePath: "", + lineNumber: 88, + hasDataSource: true, + testDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.MatrixDataSourceAttribute(), +}, + methodMetadata: __mm_9, + createInstance: __CreateInstance, + invokeBody: __Invoke, + methodIndex: 9, + createAttributes: __Attributes, + attributeGroupIndex: 0), + global::TUnit.Core.TestEntryFactory.Create( + methodName: "TrailingParamsArray_ObjectArrayFromMatrix", + fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.TrailingParamsArray_ObjectArrayFromMatrix", + filePath: "", + lineNumber: 101, + hasDataSource: true, + testDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.MatrixDataSourceAttribute(), +}, + methodMetadata: __mm_10, + createInstance: __CreateInstance, + invokeBody: __Invoke, + methodIndex: 10, + createAttributes: __Attributes, + attributeGroupIndex: 0), + global::TUnit.Core.TestEntryFactory.Create( + methodName: "ParamsObjectArray_ArrayArgumentStaysSingleElement", + fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.ParamsObjectArray_ArrayArgumentStaysSingleElement", + filePath: "", + lineNumber: 112, + hasDataSource: true, + testDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.ArgumentsAttribute(new int[] { 1, 2 }), +}, + methodMetadata: __mm_11, + createInstance: __CreateInstance, + invokeBody: __Invoke, + methodIndex: 11, + createAttributes: __Attributes, + attributeGroupIndex: 0), }; } internal static partial class TUnit_TestRegistration diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt index 2eb35fa5525..fdf27851991 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet10_0.verified.txt @@ -241,7 +241,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo } case 1: { - instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : new int[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0]))); return default(global::System.Threading.Tasks.ValueTask); } case 2: diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt index 2eb35fa5525..fdf27851991 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet8_0.verified.txt @@ -241,7 +241,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo } case 1: { - instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : new int[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0]))); return default(global::System.Threading.Tasks.ValueTask); } case 2: diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt index 2eb35fa5525..fdf27851991 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.DotNet9_0.verified.txt @@ -241,7 +241,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo } case 1: { - instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : new int[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0]))); return default(global::System.Threading.Tasks.ValueTask); } case 2: diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt index 88e68fe5d85..c4cf0b48ac6 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.DataDrivenTest_WithConflictingNamespace.Net4_7.verified.txt @@ -241,7 +241,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo } case 1: { - instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : new int[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0]))); return default(global::System.Threading.Tasks.ValueTask); } case 2: diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet10_0.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet10_0.verified.txt index e825a6dceb4..6dde667b6d5 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet10_0.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet10_0.verified.txt @@ -187,7 +187,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "strings", } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet8_0.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet8_0.verified.txt index e825a6dceb4..6dde667b6d5 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet8_0.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet8_0.verified.txt @@ -187,7 +187,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "strings", } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet9_0.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet9_0.verified.txt index e825a6dceb4..6dde667b6d5 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet9_0.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.DotNet9_0.verified.txt @@ -187,7 +187,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "strings", } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.Net4_7.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.Net4_7.verified.txt index 94309c45795..f555623cd3c 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.Net4_7.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ConflictingNamespaceTests.MethodDataSource_WithConflictingNamespace.Net4_7.verified.txt @@ -187,7 +187,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "strings", } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { diff --git a/tests/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt index 88e68fe5d85..c4cf0b48ac6 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt @@ -241,7 +241,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new glo } case 1: { - instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : new int[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + instance.IntegerArray((args[0] is null ? null : args[0] is int[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0]))); return default(global::System.Threading.Tasks.ValueTask); } case 2: diff --git a/tests/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt index 94309c45795..f555623cd3c 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt @@ -187,7 +187,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string[]), "strings", } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest((args[0] is null ? null : args[0] is string[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt index 0adeb39003c..324226980ae 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt @@ -50,7 +50,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long[]), "arr", new gl } case 2: { - instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + instance.Test(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[1]))); return default(global::System.Threading.Tasks.ValueTask); } case 3: @@ -105,7 +105,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(long[]), "arr", new gl } case 2: { - instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + instance.Test2(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[1]))); return default(global::System.Threading.Tasks.ValueTask); } case 3: diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Tests6150.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/Tests6150.Test.verified.txt index 9db1443740a..eaa0cb1d101 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/Tests6150.Test.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/Tests6150.Test.verified.txt @@ -43,7 +43,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(byte[][]), "data", new } case 1: { - return new global::System.Threading.Tasks.ValueTask(instance.JaggedArray((args[0] is null ? null : args[0] is byte[][] arr ? arr : new byte[][] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]) }))); + return new global::System.Threading.Tasks.ValueTask(instance.JaggedArray((args[0] is null ? null : args[0] is byte[][] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); } case 2: { diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 5a112227cf4..863682c5d02 100644 --- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -2224,6 +2224,7 @@ namespace .Helpers [.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" + "nctionality when AOT compiling.")] public static object? CastIfNeeded([.(..None | ..PublicParameterlessConstructor | ..PublicMethods | ..Interfaces)] type, object? value) { } + public static T[] ToTrailingArray<[.(..PublicParameterlessConstructor)] T>(object value) { } } public static class ClassConstructorHelper { diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index d88750b2f61..df987e2c496 100644 --- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -2224,6 +2224,7 @@ namespace .Helpers [.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" + "nctionality when AOT compiling.")] public static object? CastIfNeeded([.(..None | ..PublicParameterlessConstructor | ..PublicMethods | ..Interfaces)] type, object? value) { } + public static T[] ToTrailingArray<[.(..PublicParameterlessConstructor)] T>(object value) { } } public static class ClassConstructorHelper { diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 44da96916cf..8657b96496c 100644 --- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -2224,6 +2224,7 @@ namespace .Helpers [.("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break fu" + "nctionality when AOT compiling.")] public static object? CastIfNeeded([.(..None | ..PublicParameterlessConstructor | ..PublicMethods | ..Interfaces)] type, object? value) { } + public static T[] ToTrailingArray<[.(..PublicParameterlessConstructor)] T>(object value) { } } public static class ClassConstructorHelper { diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt index 6d64639253c..bc6b0a77b30 100644 --- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -2152,6 +2152,7 @@ namespace .Helpers public static object? Cast( type, object? value) { } public static T? Cast(object? value) { } public static object? CastIfNeeded( type, object? value) { } + public static T[] ToTrailingArray(object value) { } } public static class ClassConstructorHelper { diff --git a/tests/TUnit.TestProject/ArgsAsArrayTests.cs b/tests/TUnit.TestProject/ArgsAsArrayTests.cs index 97e8e30e4b8..f9a4aec15ff 100644 --- a/tests/TUnit.TestProject/ArgsAsArrayTests.cs +++ b/tests/TUnit.TestProject/ArgsAsArrayTests.cs @@ -81,4 +81,55 @@ public async Task NonParamsStringArray_BeyondStaticCaseCap(string[] names) { await Assert.That(names).IsEquivalentTo(["a", "b", "c", "d", "e", "f", "g", "h"]); } + + // Issue #6678: a data source can hand a trailing array parameter a single array whose runtime + // type differs from the parameter's — here an object[] from a MatrixAttribute subclass for a + // ConfigKind[] parameter. It must be converted element-wise, not forced into one element. + [Test] + [MatrixDataSource] + public async Task TrailingArray_ObjectArrayFromMatrix( + [MatrixArray([ConfigKind.Ai, ConfigKind.Sample], [ConfigKind.Sample, ConfigKind.NoWindow, ConfigKind.Mcp])] + ConfigKind[] configs) + { + ConfigKind[] expected = configs.Length == 2 + ? [ConfigKind.Ai, ConfigKind.Sample] + : [ConfigKind.Sample, ConfigKind.NoWindow, ConfigKind.Mcp]; + + await Assert.That(configs).IsEquivalentTo(expected); + } + + [Test] + [MatrixDataSource] + public async Task TrailingParamsArray_ObjectArrayFromMatrix( + [MatrixArray([ConfigKind.Ai, ConfigKind.Mcp])] params ConfigKind[] configs) + { + await Assert.That(configs).IsEquivalentTo([ConfigKind.Ai, ConfigKind.Mcp]); + } + + // C# params expansion is preserved: an int[] is itself an `object`, so it lands as one element + // of a `params object[]` rather than being spread element-wise. (An int[] is not an object[], + // so ArgumentsAttribute's own params binding delivers it as a single argument.) + [Test] + [Arguments(new int[] { 1, 2 })] + public async Task ParamsObjectArray_ArrayArgumentStaysSingleElement(params object[] values) + { + await Assert.That(values).HasCount(1); + await Assert.That(values[0]).IsTypeOf(); + } +} + +public enum ConfigKind +{ + Ai, + Sample, + NoWindow, + Mcp +} + +// Mirrors the community helper from discussion #4596: each constructor argument is one matrix +// value, so the decorated parameter receives a whole (object[]) array per test case. +public sealed class MatrixArrayAttribute(object[] first, object[]? second = null, object[]? third = null) : MatrixAttribute +{ + public override object?[] GetObjects(DataGeneratorMetadata dataGeneratorMetadata) + => new object?[] { first, second, third }.Where(array => array is not null).ToArray(); } From 3daf8d3932794d6939d8c4974df13f7123de7e5f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:54:55 +0100 Subject: [PATCH 2/2] fix: honour non-zero lower bound in trailing array conversion Review feedback: a rank-1 array created with an explicit lower bound (`Array.CreateInstance(typeof(object), [2], [1])`) is `Array { Rank: 1 }` but indexing it from 0 throws. Start at GetLowerBound(0) in both CastHelper.ToTrailingArray and the reflection-mode mirror, and add a regression test. Also assert element order in the new tests (IsEquivalentTo defaults to CollectionOrdering.Any). Claude-Session: https://claude.ai/code/session_015Xx3PGYRU1KjEuEmzXMMpj --- src/TUnit.Core/Helpers/CastHelper.cs | 5 +- .../Discovery/ReflectionTestDataCollector.cs | 3 +- .../ArgsAsArrayTests.Test.verified.txt | 93 ++++++++++++++++--- tests/TUnit.TestProject/ArgsAsArrayTests.cs | 30 +++++- 4 files changed, 111 insertions(+), 20 deletions(-) diff --git a/src/TUnit.Core/Helpers/CastHelper.cs b/src/TUnit.Core/Helpers/CastHelper.cs index c84875c2b7a..1383481fcc1 100644 --- a/src/TUnit.Core/Helpers/CastHelper.cs +++ b/src/TUnit.Core/Helpers/CastHelper.cs @@ -52,10 +52,13 @@ public static class CastHelper if (value is Array { Rank: 1 } source) { + // Honour a non-zero lower bound (Array.CreateInstance with explicit bounds) rather than + // assuming the source is zero-based; the result is always a normal zero-based T[]. + var lowerBound = source.GetLowerBound(0); var result = new T[source.Length]; for (var i = 0; i < result.Length; i++) { - result[i] = Cast(source.GetValue(i))!; + result[i] = Cast(source.GetValue(lowerBound + i))!; } return result; diff --git a/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs b/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs index fa5e12e0979..53c9deea31c 100644 --- a/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs +++ b/src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs @@ -1930,10 +1930,11 @@ private static bool MatchesCovariantInterface(Type paramType, Type paramGenericD // 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 sourceLowerBound = sourceArray.GetLowerBound(0); var convertedArray = Array.CreateInstance(paramsElementType, sourceArray.Length); for (var i = 0; i < sourceArray.Length; i++) { - convertedArray.SetValue(CastHelper.Cast(paramsElementType, sourceArray.GetValue(i)), i); + convertedArray.SetValue(CastHelper.Cast(paramsElementType, sourceArray.GetValue(sourceLowerBound + i)), i); } castedArgs[regularParamsCount] = convertedArray; } diff --git a/tests/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt index a2ad5867cd6..2598db6d21a 100644 --- a/tests/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt +++ b/tests/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt @@ -64,7 +64,11 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProj { global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProject.ConfigKind[]), "configs", new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.ConfigKind[])), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.ArgsAsArrayTests).GetMethod("TrailingParamsArray_ObjectArrayFromMatrix", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(global::TUnit.TestProject.ConfigKind[]) }, null)!.GetParameters()[0]) }); - private static readonly global::TUnit.Core.MethodMetadata __mm_11 = global::TUnit.Core.MethodMetadataFactory.Create("ParamsObjectArray_ArrayArgumentStaysSingleElement", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] + private static readonly global::TUnit.Core.MethodMetadata __mm_11 = global::TUnit.Core.MethodMetadataFactory.Create("TrailingArray_NonZeroLowerBoundArrayFromMatrix", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] +{ +global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::TUnit.TestProject.ConfigKind[]), "configs", new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.ConfigKind[])), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.ArgsAsArrayTests).GetMethod("TrailingArray_NonZeroLowerBoundArrayFromMatrix", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(global::TUnit.TestProject.ConfigKind[]) }, null)!.GetParameters()[0]) +}); + private static readonly global::TUnit.Core.MethodMetadata __mm_12 = global::TUnit.Core.MethodMetadataFactory.Create("ParamsObjectArray_ArrayArgumentStaysSingleElement", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[] { global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", new global::TUnit.Core.ConcreteType(typeof(object[])), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.ArgsAsArrayTests).GetMethod("ParamsObjectArray_ArrayArgumentStaysSingleElement", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(object[]) }, null)!.GetParameters()[0]) }); @@ -578,6 +582,49 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n } } case 11: + { + try + { + switch (args.Length) + { + case 0: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_NonZeroLowerBoundArrayFromMatrix(global::System.Array.Empty())); + } + case 1: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_NonZeroLowerBoundArrayFromMatrix((args[0] is null ? null : args[0] is global::TUnit.TestProject.ConfigKind[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[0])))); + } + case 2: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_NonZeroLowerBoundArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + } + case 3: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_NonZeroLowerBoundArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) })); + } + case 4: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_NonZeroLowerBoundArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) })); + } + case 5: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_NonZeroLowerBoundArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) })); + } + case 6: + { + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_NonZeroLowerBoundArrayFromMatrix(new global::TUnit.TestProject.ConfigKind[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) })); + } + default: + return new global::System.Threading.Tasks.ValueTask(instance.TrailingArray_NonZeroLowerBoundArrayFromMatrix((args.Length > 0 ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.Range(0, args.Length), i => global::TUnit.Core.Helpers.CastHelper.Cast(args[i]))) : global::System.Array.Empty()))); + } + } + catch (global::System.Exception ex) + { + return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex)); + } + } + case 12: { try { @@ -645,7 +692,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "Params", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.Params", filePath: "", - lineNumber: 5, + lineNumber: 7, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -661,7 +708,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "ParamsEnumerable", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.ParamsEnumerable", filePath: "", - lineNumber: 15, + lineNumber: 17, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -677,7 +724,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "Following_Non_Params", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.Following_Non_Params", filePath: "", - lineNumber: 25, + lineNumber: 27, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -693,7 +740,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "NonParamsStringArray", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsStringArray", filePath: "", - lineNumber: 38, + lineNumber: 40, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -710,7 +757,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "NonParamsStringArray_SingleValue", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsStringArray_SingleValue", filePath: "", - lineNumber: 47, + lineNumber: 49, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -726,7 +773,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "NonParamsStringArray_MultipleValues", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsStringArray_MultipleValues", filePath: "", - lineNumber: 54, + lineNumber: 56, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -742,7 +789,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "NonParamsIntArray", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsIntArray", filePath: "", - lineNumber: 61, + lineNumber: 63, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -758,7 +805,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "GenericStringArray", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.GenericStringArray", filePath: "", - lineNumber: 69, + lineNumber: 71, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -774,7 +821,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "NonParamsStringArray_BeyondStaticCaseCap", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsStringArray_BeyondStaticCaseCap", filePath: "", - lineNumber: 78, + lineNumber: 80, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -790,7 +837,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "TrailingArray_ObjectArrayFromMatrix", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.TrailingArray_ObjectArrayFromMatrix", filePath: "", - lineNumber: 88, + lineNumber: 90, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -806,7 +853,7 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodName: "TrailingParamsArray_ObjectArrayFromMatrix", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.TrailingParamsArray_ObjectArrayFromMatrix", filePath: "", - lineNumber: 101, + lineNumber: 103, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { @@ -818,20 +865,36 @@ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object[]), "values", n methodIndex: 10, createAttributes: __Attributes, attributeGroupIndex: 0), + global::TUnit.Core.TestEntryFactory.Create( + methodName: "TrailingArray_NonZeroLowerBoundArrayFromMatrix", + fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.TrailingArray_NonZeroLowerBoundArrayFromMatrix", + filePath: "", + lineNumber: 113, + hasDataSource: true, + testDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.MatrixDataSourceAttribute(), +}, + methodMetadata: __mm_11, + createInstance: __CreateInstance, + invokeBody: __Invoke, + methodIndex: 11, + createAttributes: __Attributes, + attributeGroupIndex: 0), global::TUnit.Core.TestEntryFactory.Create( methodName: "ParamsObjectArray_ArrayArgumentStaysSingleElement", fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.ParamsObjectArray_ArrayArgumentStaysSingleElement", filePath: "", - lineNumber: 112, + lineNumber: 124, hasDataSource: true, testDataSources: new global::TUnit.Core.IDataSourceAttribute[] { new global::TUnit.Core.ArgumentsAttribute(new int[] { 1, 2 }), }, - methodMetadata: __mm_11, + methodMetadata: __mm_12, createInstance: __CreateInstance, invokeBody: __Invoke, - methodIndex: 11, + methodIndex: 12, createAttributes: __Attributes, attributeGroupIndex: 0), }; diff --git a/tests/TUnit.TestProject/ArgsAsArrayTests.cs b/tests/TUnit.TestProject/ArgsAsArrayTests.cs index f9a4aec15ff..565d219b625 100644 --- a/tests/TUnit.TestProject/ArgsAsArrayTests.cs +++ b/tests/TUnit.TestProject/ArgsAsArrayTests.cs @@ -1,4 +1,6 @@ -namespace TUnit.TestProject; +using TUnit.Assertions.Enums; + +namespace TUnit.TestProject; public class ArgsAsArrayTests { @@ -95,7 +97,7 @@ public async Task TrailingArray_ObjectArrayFromMatrix( ? [ConfigKind.Ai, ConfigKind.Sample] : [ConfigKind.Sample, ConfigKind.NoWindow, ConfigKind.Mcp]; - await Assert.That(configs).IsEquivalentTo(expected); + await Assert.That(configs).IsEquivalentTo(expected, CollectionOrdering.Matching); } [Test] @@ -103,7 +105,17 @@ public async Task TrailingArray_ObjectArrayFromMatrix( public async Task TrailingParamsArray_ObjectArrayFromMatrix( [MatrixArray([ConfigKind.Ai, ConfigKind.Mcp])] params ConfigKind[] configs) { - await Assert.That(configs).IsEquivalentTo([ConfigKind.Ai, ConfigKind.Mcp]); + await Assert.That(configs).IsEquivalentTo([ConfigKind.Ai, ConfigKind.Mcp], CollectionOrdering.Matching); + } + + // A rank-1 array with a non-zero lower bound (`object[*]`) must convert too — the loop has to + // start at GetLowerBound(0), not 0. + [Test] + [MatrixDataSource] + public async Task TrailingArray_NonZeroLowerBoundArrayFromMatrix( + [NonZeroLowerBoundMatrix] ConfigKind[] configs) + { + await Assert.That(configs).IsEquivalentTo([ConfigKind.NoWindow, ConfigKind.Mcp], CollectionOrdering.Matching); } // C# params expansion is preserved: an int[] is itself an `object`, so it lands as one element @@ -133,3 +145,15 @@ public sealed class MatrixArrayAttribute(object[] first, object[]? second = null public override object?[] GetObjects(DataGeneratorMetadata dataGeneratorMetadata) => new object?[] { first, second, third }.Where(array => array is not null).ToArray(); } + +// Supplies a single rank-1 object array whose lower bound is 1 rather than 0. +public sealed class NonZeroLowerBoundMatrixAttribute : MatrixAttribute +{ + public override object?[] GetObjects(DataGeneratorMetadata dataGeneratorMetadata) + { + var array = Array.CreateInstance(typeof(object), [2], [1]); + array.SetValue(ConfigKind.NoWindow, 1); + array.SetValue(ConfigKind.Mcp, 2); + return [array]; + } +}