Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
272 changes: 42 additions & 230 deletions TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ internal sealed class Tests_Test_TestSource_GUID : global::TUnit.Core.Interfaces
typedInstance.Test(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[0]);
break;
case 2:
typedInstance.Test(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]) });
typedInstance.Test(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]) }));
break;
case 3:
typedInstance.Test(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]), TUnit.Core.Helpers.CastHelper.Cast<long>(args[2]) });
Expand Down Expand Up @@ -115,7 +115,7 @@ internal sealed class Tests_Test_TestSource_GUID : global::TUnit.Core.Interfaces
instance.Test(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[0]);
break;
case 2:
instance.Test(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]) });
instance.Test(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]) }));
break;
case 3:
instance.Test(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]), TUnit.Core.Helpers.CastHelper.Cast<long>(args[2]) });
Expand Down Expand Up @@ -242,7 +242,7 @@ internal sealed class Tests_Test2_TestSource_GUID : global::TUnit.Core.Interface
typedInstance.Test2(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[0]);
break;
case 2:
typedInstance.Test2(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]) });
typedInstance.Test2(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]) }));
break;
case 3:
typedInstance.Test2(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]), TUnit.Core.Helpers.CastHelper.Cast<long>(args[2]) });
Expand Down Expand Up @@ -272,7 +272,7 @@ internal sealed class Tests_Test2_TestSource_GUID : global::TUnit.Core.Interface
instance.Test2(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[0]);
break;
case 2:
instance.Test2(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]) });
instance.Test2(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]) }));
break;
case 3:
instance.Test2(TUnit.Core.Helpers.CastHelper.Cast<int>(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast<long>(args[1]), TUnit.Core.Helpers.CastHelper.Cast<long>(args[2]) });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,34 +139,34 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
// Handle params array parameter
var paramsParam = parameters[parameters.Count - 1];
var elementType = (paramsParam.Type as IArrayTypeSymbol)?.ElementType;

if (elementType != null)
{
if (argCountExpression != null)
{
// Dynamic count - create array from remaining arguments
var arrayInit = $@"
({argumentsArrayName}.Length > {regularParamCount}
? global::System.Linq.Enumerable.Range({regularParamCount}, {argCountExpression} - {regularParamCount})
.Select(i => TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({argumentsArrayName}[i]))
.ToArray()
: new {elementType.GloballyQualified()}[0])";
argumentExpressions.Add(arrayInit.Replace("\r\n", " ").Replace("\n", " ").Trim());
var arrayInit = $"({argumentsArrayName}.Length > {regularParamCount} ? global::System.Linq.Enumerable.Range({regularParamCount}, {argCountExpression} - {regularParamCount}).Select(i => TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({argumentsArrayName}[i])).ToArray() : new {elementType.GloballyQualified()}[0])";
argumentExpressions.Add(arrayInit);
}
else
{
var remainingArgCount = Math.Max(0, argCount - regularParamCount);

if (remainingArgCount == 0)
{
// No arguments for params array - pass empty array
argumentExpressions.Add($"new {elementType.GloballyQualified()}[0]");
}
else if (remainingArgCount == 1)
{
// Single argument for params - create array with single element
var singleElementExpression = $"TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({argumentsArrayName}[{regularParamCount}])";
argumentExpressions.Add($"new {elementType.GloballyQualified()}[] {{ {singleElementExpression} }}");
// Single argument for params - check if it's null or already the correct array type
// In C#, params T[] can receive:
// - 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)
var singleArg = $"{argumentsArrayName}[{regularParamCount}]";
var checkAndCast = $"({singleArg} is null ? null : {singleArg} is {paramsParam.Type.GloballyQualified()} arr ? arr : new {elementType.GloballyQualified()}[] {{ TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({singleArg}) }})";
argumentExpressions.Add(checkAndCast);
}
else
{
Expand Down
66 changes: 53 additions & 13 deletions TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1566,32 +1566,72 @@ private static bool IsCovariantCompatible(Type paramType, [DynamicallyAccessedMe

if (paramsElementType != null)
{
var paramsArray = Array.CreateInstance(paramsElementType, paramsCount);
for (var i = 0; i < paramsCount; i++)
// C# params semantics:
// - If single arg is null, pass null (not array with null element)
// - If single arg is already the correct array type, pass it directly
// - Otherwise, create array from remaining arguments
if (paramsCount == 1)
{
var arg = args[paramsStartIndex + i];
if (arg != null)
var singleArg = args[paramsStartIndex];
if (singleArg == null)
{
var argType = arg.GetType();
if (paramsElementType.IsAssignableFrom(argType))
// Null should be passed as null, not as array with null element
castedArgs[regularParamsCount] = null;
}
else if (lastParam.ParameterType.IsAssignableFrom(singleArg.GetType()))
{
// If the argument is already the correct array type, use it directly
castedArgs[regularParamsCount] = singleArg;
}
else
{
// Single non-array argument - wrap in array
var singleElementArray = Array.CreateInstance(paramsElementType, 1);
if (paramsElementType.IsAssignableFrom(singleArg.GetType()))
{
paramsArray.SetValue(arg, i);
singleElementArray.SetValue(singleArg, 0);
}
else if (IsCovariantCompatible(paramsElementType, argType))
else if (IsCovariantCompatible(paramsElementType, singleArg.GetType()))
{
paramsArray.SetValue(arg, i);
singleElementArray.SetValue(singleArg, 0);
}
else
{
paramsArray.SetValue(CastHelper.Cast(paramsElementType, arg), i);
singleElementArray.SetValue(CastHelper.Cast(paramsElementType, singleArg), 0);
}
castedArgs[regularParamsCount] = singleElementArray;
}
else
}
else
{
// Multiple arguments or no arguments - create array
var paramsArray = Array.CreateInstance(paramsElementType, paramsCount);
for (var i = 0; i < paramsCount; i++)
{
paramsArray.SetValue(null, i);
var arg = args[paramsStartIndex + i];
if (arg != null)
{
var argType = arg.GetType();
if (paramsElementType.IsAssignableFrom(argType))
{
paramsArray.SetValue(arg, i);
}
else if (IsCovariantCompatible(paramsElementType, argType))
{
paramsArray.SetValue(arg, i);
}
else
{
paramsArray.SetValue(CastHelper.Cast(paramsElementType, arg), i);
}
}
else
{
paramsArray.SetValue(null, i);
}
}
castedArgs[regularParamsCount] = paramsArray;
}
castedArgs[regularParamsCount] = paramsArray;
}
}
else
Expand Down
22 changes: 1 addition & 21 deletions TUnit.TestProject/ArgsAsArrayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,6 @@ public void Params(params string[] arguments)
}
}

[Test]
[Arguments("arg1", "arg2", "arg3")]
public void NonParams(string[] arguments)
{
foreach (var argument in arguments)
{
Console.WriteLine(argument);
}
}

[Test]
[Arguments("arg1", "arg2", "arg3")]
public void ParamsEnumerable(params IEnumerable<string> arguments)
Expand All @@ -32,19 +22,9 @@ public void ParamsEnumerable(params IEnumerable<string> arguments)
}
}

[Test]
[Arguments("arg1", "arg2", "arg3")]
public void Enumerable(IEnumerable<string> arguments)
{
foreach (var argument in arguments)
{
Console.WriteLine(argument);
}
}

[Test]
[Arguments(1, "arg1", "arg2", "arg3")]
public void Following_Non_Params(int i, IEnumerable<string> arguments)
public void Following_Non_Params(int i, params IEnumerable<string> arguments)
{
foreach (var argument in arguments)
{
Expand Down
66 changes: 66 additions & 0 deletions TUnit.TestProject/Issue2589Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace TUnit.TestProject;

/// <summary>
/// Tests for issue #2589: Data driven tests should use params keyword
/// https://github.com/thomhurst/TUnit/issues/2589
/// </summary>
public class Issue2589Tests
{
[Test]
[Arguments("sample", "param1", "param2", "param3")]
public async Task SampleWithParamsArguments(string text, params string[] tags)
{
await Assert.That(text).IsEqualTo("sample");
await Assert.That(tags).IsNotNull();
await Assert.That(tags.Length).IsEqualTo(3);
await Assert.That(tags[0]).IsEqualTo("param1");
await Assert.That(tags[1]).IsEqualTo("param2");
await Assert.That(tags[2]).IsEqualTo("param3");
}

[Test]
[Arguments("sample", null)]
public async Task SampleWithNull(string text, params string[]? tags)
{
await Assert.That(text).IsEqualTo("sample");
await Assert.That(tags).IsNull();
}

[Test]
[Arguments("sample", null)]
public async Task NonParamsWithNull(string text, string[]? tags)
{
await Assert.That(text).IsEqualTo("sample");
await Assert.That(tags).IsNull();
}

[Test]
[Arguments("sample")]
public async Task ParamsWithNoExtraArgs(string text, params string[] tags)
{
await Assert.That(text).IsEqualTo("sample");
await Assert.That(tags).IsNotNull();
await Assert.That(tags.Length).IsEqualTo(0);
}

[Test]
[Arguments("sample", "")]
public async Task ParamsWithEmptyString(string text, params string[] tags)
{
await Assert.That(text).IsEqualTo("sample");
await Assert.That(tags).IsNotNull();
await Assert.That(tags.Length).IsEqualTo(1);
await Assert.That(tags[0]).IsEqualTo("");
}

[Test]
[Arguments("sample", "", "other")]
public async Task ParamsWithEmptyStringAndOthers(string text, params string[] tags)
{
await Assert.That(text).IsEqualTo("sample");
await Assert.That(tags).IsNotNull();
await Assert.That(tags.Length).IsEqualTo(2);
await Assert.That(tags[0]).IsEqualTo("");
await Assert.That(tags[1]).IsEqualTo("other");
}
}
Loading